You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

使用scipy.stats.chi2_contingency遇ValueError求无常数修复方案

卡方决策树中chi2_contingency零期望频率错误的正确修复方案

问题背景

使用scipy.stats.chi2_contingency实现卡方决策树时,触发以下错误:

ValueError: The internally computed table of expected frequencies has a zero element at (0, 0)

目前找到的临时解决方案是给类别计数添加1e-10小常数,但该方案属于hack性质,会干扰统计结果的准确性,需要更合理的修复方式。

错误根源

问题出在类别计数的生成逻辑:
原代码使用np.bincount(y_left, minlength=len(classes))计算子集类别计数,但classes是从样本标签y中提取的集合(可能不是从0开始的连续整数)。np.bincount会基于标签值的索引生成数组,导致数组中包含原始数据不存在的类别(比如标签从1开始时,索引0对应的计数为0)。这些不存在的类别在左右子集的计数均为0,形成列联表中全0的列,计算期望频数时就会出现0值,触发scipy的报错。

正确修复方案

放弃np.bincount,直接针对classes中的每个真实类别统计子集内的样本数,确保列联表只包含实际存在的类别,避免全0列的出现。

修改代码片段

在best_split函数中,替换原有的left_counts和right_counts计算代码:

# 替换原bincount实现
left_counts = np.array([np.sum(y_left == c) for c in classes])
right_counts = np.array([np.sum(y_right == c) for c in classes])

修复原理

  • 仅统计classes中存在的真实类别,不会引入不存在的类别导致全0列
  • 每个类别的列和为原始数据中该类别的样本数(必然大于0),行和为左/右子集的样本数(拆分时保证子集至少有1个样本,必然大于0)
  • 所有期望频数E = (行和 * 列和)/总样本数均为正数,完全满足卡方检验的前提条件

修改后的完整代码

import numpy as np
from scipy.stats import chi2_contingency
import matplotlib.pyplot as plt

class Node_chi2:
    def __init__(self, chi2, num_samples, num_samples_per_class, predicted_class):
        self.chi2 = chi2
        self.num_samples = num_samples
        self.num_samples_per_class = num_samples_per_class
        self.predicted_class = predicted_class
        self.feature_index = 0
        self.threshold = 0
        self.left = None
        self.right = None

class DecisionTree_chi2():

    def __init__(self, max_depth=None):
        self.max_depth = max_depth

    def fit(self, X, y, depth=0):
        classes = list(set(y))
        num_samples_per_class = [np.sum(y == i) for i in classes]
        predicted_class = classes[np.argmax(num_samples_per_class)]
        node = Node_chi2(
              chi2=0,  # chi2 is 0 at the start
              num_samples=len(y),
              num_samples_per_class=num_samples_per_class,
              predicted_class=predicted_class,
          )

        if depth < self.max_depth:
            idx, thr = self.best_split(X, y, classes)
            if idx is not None:
                indices_left = X[:, idx] < thr
                X_left, y_left = X[indices_left], y[indices_left]
                X_right, y_right = X[~indices_left], y[~indices_left]
                node.feature_index = idx
                node.threshold = thr
                node.left = self.fit(X_left, y_left, depth + 1)
                node.right = self.fit(X_right, y_right, depth + 1)

        self.node = node
        return self.node
    
    def best_split(self, X, y, classes):
        m, n = X.shape
        if m <= 1:
            return None, None

        best_chi2 = 0  # Best chi2 score
        best_idx, best_thr = None, None

        for idx in range(n):
            thresholds, classes_sorted = zip(*sorted(zip(X[:, idx], y)))
            for i in range(1, m):  # iterate through each threshold
                if thresholds[i] == thresholds[i - 1]:
                    continue
                y_left = np.array(classes_sorted[:i])
                y_right = np.array(classes_sorted[i:])
                # 修复后的类别计数逻辑
                left_counts = np.array([np.sum(y_left == c) for c in classes])
                right_counts = np.array([np.sum(y_right == c) for c in classes])
                chi2, p, _ , _ = chi2_contingency(np.array([left_counts, right_counts]))

                if chi2 > best_chi2:
                    best_chi2 = chi2
                    best_idx = idx
                    best_thr = (thresholds[i] + thresholds[i - 1]) / 2

        return best_idx, best_thr

    def predict(self, X):
        yhat = []
        for sample in X: 
            node = self.node
            while node.left:
                if sample[node.feature_index] < node.threshold:
                    node = node.left
                else:
                    node = node.right
            yhat.append(node.predicted_class)
        return np.array(yhat)

from sklearn.datasets import make_moons, make_classification

X, y = make_moons(n_samples=100, noise=0.1, random_state=42)

def plot_decision_boundaries(clf, X, y, label=''):
    # Generate a grid of points to make predictions:
    x1_min, x1_max = X[:, 0].min() - 1, X[:, 0].max() + 1
    x2_min, x2_max = X[:, 1].min() - 1, X[:, 1].max() + 1
    xx1, xx2 = np.meshgrid(np.arange(x1_min, x1_max, 0.1), np.arange(x2_min, x2_max, 0.1))

    # Use the classifier to make predictions on the grid:
    Z = clf.predict(np.c_[xx1.ravel(), xx2.ravel()])

    Z = Z.reshape(xx1.shape)

    colors = {0: 'C0', 1: 'C1', 2: 'C2'}
    # colors dictionary indexed by class label

    # Create a contour plot to display the decision boundaries:
    fig = plt.figure(figsize=(4, 4))

    from matplotlib.colors import ListedColormap
    n_classes = len(np.unique(y))
    custom_cmap = ListedColormap(list(colors.values())[:n_classes])

    plt.contourf(xx1, xx2, Z, cmap=custom_cmap, alpha=0.4)

    plt.scatter(X[y==0,0], X[y==0,1], c=colors[0], marker='o', edgecolors='k', alpha=0.5)
    plt.scatter(X[y==1,0], X[y==1,1], c=colors[1], marker='o', edgecolors='k', alpha=0.5)
    plt.scatter(X[y==2,0], X[y==2,1], c=colors[2], marker='o', edgecolors='k', alpha=0.5)

    plt.xlabel('X1')
    plt.ylabel('X2')
    plt.title('Decision Boundaries - '+label+' DT')
    plt.show()


clf_gini = DecisionTree_chi2(max_depth=5)
clf_gini.fit(X, y)

plot_decision_boundaries(clf_gini, X, y, label='Chi2')

关于临时方案的说明

添加1e-10的临时方案虽然能绕过报错,但会人为修改观测频数,影响卡方值的计算准确性,尤其是样本量较小时,误差会被放大,因此不推荐使用。

内容的提问来源于stack exchange,提问作者Visage

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.06 07:45:55