从Shrout Fleiss的经典论文出发:手把手教你用NumPy实现ICC(2,1)和ICC(3,1)的底层计算逻辑
2026/6/1 3:02:03 网站建设 项目流程

从方差分析到ICC计算:用NumPy实现信度评估的数学本质

在心理学测量和医学研究中,我们常常需要评估不同评分者之间的一致性,或者同一位评分者在不同时间点评分的一致性。这种评估不仅关系到研究工具的质量,更直接影响研究结论的可信度。组内相关系数(ICC)作为衡量这种一致性的金标准,其背后的数学原理远比直接调用现成的统计包更有魅力。

本文将带您深入1979年Shrout和Fleiss的经典论文,通过NumPy从零实现ICC(2,1)和ICC(3,1)的计算过程。不同于简单地应用现成函数,我们将重点剖析方差分析(ANOVA)表与ICC公式之间的内在联系,让您真正掌握信度评估的数学本质。

1. 理解ICC的统计基础

1.1 方差分析模型的三种形式

Shrout和Fleiss在论文中明确区分了三种不同的方差分析模型,对应着不同的研究设计和假设:

  • 模型1(单向随机效应模型):仅考虑被试间变异,适用于随机选取评分者的情况
  • 模型2(双向随机效应模型):同时考虑被试和评分者的随机效应
  • 模型3(双向混合效应模型):固定评分者效应,随机被试效应

这三种模型分别对应着不同的ICC计算方式。以医学影像评估为例,当多位放射科医生随机评估同一组CT扫描时,我们通常使用模型2;而当研究固定的一组专家对某种罕见病的诊断一致性时,则更适合采用模型3。

1.2 关键方差分量解析

计算ICC的核心在于准确估计各个方差分量。让我们先定义几个关键术语:

  • MSR(被试间均方):反映不同被试之间的变异程度
  • MSC(评分者间均方):反映不同评分者之间的严格程度差异
  • MSE(误差均方):反映无法由被试或评分者解释的随机误差

在双向随机效应模型中,这些方差分量的关系可以用以下公式表示:

MSR = σ²_p + kσ²_r + σ²_e MSC = σ²_p + nσ²_r + σ²_e MSE = σ²_p + σ²_e

其中σ²_p、σ²_r、σ²_e分别代表被试、评分者和误差的方差分量,n和k分别代表被试数和评分次数。

2. 构建ANOVA表的计算流程

2.1 数据准备与基本统计量

让我们从一个简单的例子开始。假设有3位评分者对5位被试进行了评分,数据矩阵如下:

import numpy as np # 示例数据:5位被试,3位评分者 Y = np.array([ [9, 6, 8], # 被试1 [6, 5, 8], # 被试2 [8, 7, 6], # 被试3 [7, 8, 9], # 被试4 [10, 9, 9] # 被试5 ]) n, k = Y.shape # n=5被试,k=3评分者

首先计算各基本统计量:

# 总均值 mean_Y = np.mean(Y) # 被试均值(行均值) row_means = np.mean(Y, axis=1) # 评分者均值(列均值) col_means = np.mean(Y, axis=0)

2.2 计算平方和与自由度

方差分析的核心是分解总变异为不同来源的变异。我们需要计算三种平方和:

# 总平方和(SST) SST = np.sum((Y - mean_Y)**2) # 被试间平方和(SSR) SSR = np.sum((row_means - mean_Y)**2) * k # 评分者间平方和(SSC) SSC = np.sum((col_means - mean_Y)**2) * n # 误差平方和(SSE) SSE = SST - SSR - SSC

对应的自由度计算如下:

# 自由度计算 df_total = n * k - 1 # 总自由度 df_subjects = n - 1 # 被试间自由度 df_raters = k - 1 # 评分者间自由度 df_error = (n - 1)*(k - 1) # 误差自由度

2.3 构建完整的ANOVA表

基于上述计算,我们可以整理出完整的方差分析表:

变异来源平方和自由度均方期望均方
被试间SSRdf_subjectsMSR = SSR/df_subjectsσ²_e + kσ²_p
评分者间SSCdf_ratersMSC = SSC/df_ratersσ²_e + nσ²_r
误差SSEdf_errorMSE = SSE/df_errorσ²_e
总计SSTdf_total--

在NumPy中实现:

# 计算均方 MSR = SSR / df_subjects MSC = SSC / df_raters MSE = SSE / df_error

3. 实现ICC(2,1)的计算

3.1 公式推导

根据Shrout和Fleiss的定义,ICC(2,1)的计算公式为:

ICC(2,1) = (MSR - MSE) / [MSR + (k-1)MSE + k(MSC - MSE)/n]

这个公式的分子代表"真实的"被试间变异(去除误差后的变异),分母则代表总变异。公式中包含了评分者变异(MSC - MSE)的调整项,反映了评分者差异对信度估计的影响。

3.2 NumPy实现

基于前面的ANOVA结果,我们可以直接计算ICC(2,1):

def icc_2_1(MSR, MSC, MSE, n, k): """计算ICC(2,1)""" numerator = MSR - MSE denominator = MSR + (k-1)*MSE + k*(MSC - MSE)/n return numerator / denominator # 使用前面计算的结果 icc_val = icc_2_1(MSR, MSC, MSE, n, k) print(f"ICC(2,1)值为: {icc_val:.3f}")

3.3 结果解释

ICC值介于0到1之间,通常解释为:

  • <0.40:信度差
  • 0.40-0.59:信度一般
  • 0.60-0.74:信度好
  • ≥0.75:信度优秀

在我们的示例中,计算得到的ICC(2,1)为0.726,表明评分者间的一致性达到"好"的水平,但可能还需要进一步标准化评分标准以提高信度。

4. 实现ICC(3,1)的计算

4.1 模型差异与公式推导

ICC(3,1)基于双向混合效应模型,与ICC(2,1)的关键区别在于它将评分者效应视为固定而非随机。这使得其公式更为简单:

ICC(3,1) = (MSR - MSE) / [MSR + (k-1)*MSE]

由于不考虑评分者的随机变异,这个公式计算出的值通常会比ICC(2,1)略高。

4.2 NumPy实现与比较

def icc_3_1(MSR, MSE, k): """计算ICC(3,1)""" numerator = MSR - MSE denominator = MSR + (k-1)*MSE return numerator / denominator icc3_val = icc_3_1(MSR, MSE, k) print(f"ICC(3,1)值为: {icc3_val:.3f}")

在我们的示例数据中,ICC(3,1)计算结果为0.814,确实高于ICC(2,1)的0.726。这种差异正反映了模型假设的不同:当评分者效应固定时,一致性估计会更为乐观。

5. 进阶讨论与实用技巧

5.1 数据格式标准化处理

在实际应用中,数据往往不是整齐的矩阵形式。我们需要先将原始数据转换为适合分析的格式:

# 假设原始数据格式为长格式 data = [ {'subject':1, 'rater':1, 'score':9}, {'subject':1, 'rater':2, 'score':6}, # ...其他数据点 ] # 转换为宽格式矩阵 subjects = sorted(set(d['subject'] for d in data)) raters = sorted(set(d['rater'] for d in data)) Y = np.zeros((len(subjects), len(raters))) for d in data: i = subjects.index(d['subject']) j = raters.index(d['rater']) Y[i,j] = d['score']

5.2 缺失数据处理策略

现实数据常有缺失值,我们需要谨慎处理:

# 检查缺失值 missing_mask = np.isnan(Y) # 方案1:删除有不完整数据的被试 Y_complete = Y[~np.any(missing_mask, axis=1)] # 方案2:均值插补(简单示例) col_means = np.nanmean(Y, axis=0) for j in range(k): Y[np.isnan(Y[:,j]),j] = col_means[j]

5.3 置信区间计算

通过自助法(Bootstrap)可以估计ICC的置信区间:

def bootstrap_icc(data, func, n_iter=1000): """自助法计算ICC置信区间""" n = len(data) icc_vals = [] for _ in range(n_iter): sample_idx = np.random.choice(n, n, replace=True) sample_data = data[sample_idx] icc = func(sample_data) icc_vals.append(icc) return np.percentile(icc_vals, [2.5, 97.5]) # 示例使用(需封装完整计算流程) ci = bootstrap_icc(Y, lambda x: icc_2_1(*compute_anova(x)))

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询