概要¶
Differential Item Functioning(DIF: 特異項目機能) とは、同じ能力を持つ異なる集団の受験者が、特定のテスト項目に対して異なる正答確率を示す現象である。
例えば、同じ数学的能力を持つ男女が、ある文章題に対して異なる正答率を示す場合、その項目にはDIFが存在する可能性がある。これは項目の内容が一方の集団に有利に働いていることを示唆し、テストの公平性(fairness)に関わる重要な問題である。
DIF分析では通常、比較対象となる2つの集団を 参照群(reference group) と 焦点群(focal group) と呼ぶ。参照群は基準となる集団、焦点群はDIFの有無を検討したい集団を指す。
IRTにおける定式化¶
2PLM(2パラメータロジスティックモデル)を考える。集団 に属する受験者 が項目 に正答する確率は
と表せる。ここで
:集団 における項目 の識別力(discrimination)
:集団 における項目 の困難度(difficulty)
DIFが存在しない項目では、これらのパラメータは集団によらず一定である(、)。DIFはこの等質性のいずれか、あるいは両方が崩れる現象として定式化される。
DIFの種類¶
DIFは、集団間の差が能力レベルに依存するかどうかによって、Uniform DIF(均一DIF)とNon-uniform DIF(非均一DIF)の2種類に分けられる。
Uniform DIF(均一DIF)¶
能力レベルによらず、一方の集団が常に有利(または不利)である場合をUniform DIFという。識別力 は集団間で等しいまま、困難度 のみが集団間で一定量シフトする。
は焦点群(集団2)における困難度の、参照群(集団1)からのシフト量である。
ICC(Item Characteristic Curve, 項目特性曲線)上では、Uniform DIFは次のように現れる。
2つの集団のICCは傾きが同じ
ICCは の分だけ水平方向に平行移動する
したがって、能力水準 によらず**一方の集団が一貫して有利(または不利)**になる
Non-uniform DIF(非均一DIF)¶
集団間の差が能力レベルによって変化する場合をNon-uniform DIFという。低能力層では参照群が有利だが高能力層では焦点群が有利になる、といった逆転が起こりうる。IRTでは識別力パラメータが集団間で異なることに対応する。
ICC上では傾きが集団間で異なるため、2本のICCが交差する(crossing ICC)。DIFの大きさと方向が に依存する点がUniform DIFとの違いである。
階層ベイズによる定式化¶
比較する集団の数が多い場合(例えば都道府県や学校など)、集団ごとの困難度シフトを固定効果として個別に推定すると過学習しやすい。この場合、シフト量に階層事前分布を置いて部分プーリングする定式化が有用である。
は項目 のベースライン困難度、 は集団 固有のずれである。集団ごとのサンプルサイズが小さくても安定した推定が得られやすいという利点があり、Uniform DIF(ICCの平行移動のみを許容するモデル)の階層ベイズ版とみなせる。
多値項目への拡張¶
ここまでは二値項目(正誤)を前提とする2PLMで説明してきたが、GPCM(Generalized Partial Credit Model)やGRM(Graded Response Model)のような多値項目向けのIRTモデルにもDIFの考え方は拡張できる。この場合、困難度パラメータ の代わりに、各カテゴリの閾値パラメータ (はカテゴリ)が集団間でシフトするかどうかを検討する。
全閾値が同じ量だけシフトする場合がUniform DIFに、閾値ごとにシフト量や識別力が異なる場合がNon-uniform DIFに対応する。
DIF検出手法¶
実際のデータからDIFを検出するための代表的な手法を紹介する。いずれも、能力を揃えたときに参照群と焦点群の間で正答率(または項目パラメータ)に差があるかどうかを検定する点で共通する。
Mantel-Haenszel法¶
最も広く使用されているDIF検出手法の一つ。能力の代理指標として総得点を用い、各得点層で2×2の分割表を作成し、集団間の正答オッズ比を検定する。
Mantel-Haenszel統計量:
ここで は得点層 における参照群の正答者数、 はその期待値。
共通オッズ比の推定値:
ETSでは、この対数を変換したDelta尺度(MH D-DIF)がよく使われる:
ロジスティック回帰法¶
項目への正答を目的変数、能力(総得点)と集団を説明変数とするロジスティック回帰を用いる方法。
: 能力(または総得点)
: 集団を示すダミー変数
: Uniform DIFの存在
: Non-uniform DIFの存在
この方法の利点は、Uniform DIFとNon-uniform DIFを同時に検出できることである。
IRT法¶
各集団で別々にIRTモデルを推定し、DIFがないと仮定される アンカー項目(anchor item) を用いて尺度を等化したのち、項目パラメータの差を検定する方法。
Lord’s 検定:
ここで は項目パラメータのベクトル、 はその共分散行列の推定値。
SIBTEST¶
Shealy & Stout (1993) が開発した、多次元性に基づくDIF検出法。項目をアンカー項目(DIFがないと仮定)と検討項目に分け、条件付き期待得点の差を検定する。
Pythonでの実装¶
difNLR パッケージ(R)¶
Rには difR や difNLR など、DIF分析に特化したパッケージがある。
library(difNLR)
# Mantel-Haenszel法
difMH(Data, group, focal.name = 1)
# ロジスティック回帰法
difLogistic(Data, group, focal.name = 1, type = "both")Pythonによる実装例¶
PythonでのDIF分析は、statsmodels を用いたロジスティック回帰や、scipy.stats を用いたMantel-Haenszel検定で実装できる。
import numpy as np
import pandas as pd
from scipy import stats
import statsmodels.api as sm
from statsmodels.formula.api import logitdef mantel_haenszel_dif(response: np.ndarray, group: np.ndarray, total_score: np.ndarray):
"""
Mantel-Haenszel法によるDIF検出
Parameters
----------
response : np.ndarray
項目への応答(0/1)
group : np.ndarray
集団(0: 参照群, 1: 焦点群)
total_score : np.ndarray
総得点(能力の代理指標)
Returns
-------
dict
MH統計量、p値、共通オッズ比、MH D-DIF
"""
# 得点層ごとに分割表を作成
unique_scores = np.unique(total_score)
A_sum = 0 # 参照群・正答
B_sum = 0 # 参照群・誤答
C_sum = 0 # 焦点群・正答
D_sum = 0 # 焦点群・誤答
numerator = 0
denominator = 0
expected_A = 0
var_A = 0
for score in unique_scores:
mask = total_score == score
A = np.sum((group[mask] == 0) & (response[mask] == 1)) # 参照群・正答
B = np.sum((group[mask] == 0) & (response[mask] == 0)) # 参照群・誤答
C = np.sum((group[mask] == 1) & (response[mask] == 1)) # 焦点群・正答
D = np.sum((group[mask] == 1) & (response[mask] == 0)) # 焦点群・誤答
N = A + B + C + D
if N == 0:
continue
n1 = A + B # 参照群の人数
n2 = C + D # 焦点群の人数
m1 = A + C # 正答者数
m2 = B + D # 誤答者数
# オッズ比の計算用
numerator += A * D / N
denominator += B * C / N
# MH統計量の計算用
expected_A += n1 * m1 / N
var_A += n1 * n2 * m1 * m2 / (N**2 * (N - 1)) if N > 1 else 0
# 共通オッズ比
alpha_mh = numerator / denominator if denominator > 0 else np.nan
# MH D-DIF(ETS Delta尺度)
mh_d_dif = -2.35 * np.log(alpha_mh) if alpha_mh > 0 else np.nan
# MHカイ二乗統計量
observed_A = np.sum((group == 0) & (response == 1))
chi2_mh = (abs(observed_A - expected_A) - 0.5)**2 / var_A if var_A > 0 else np.nan
p_value = 1 - stats.chi2.cdf(chi2_mh, df=1)
return {
'chi2_mh': chi2_mh,
'p_value': p_value,
'alpha_mh': alpha_mh,
'mh_d_dif': mh_d_dif
}def logistic_regression_dif(response: np.ndarray, group: np.ndarray, total_score: np.ndarray):
"""
ロジスティック回帰法によるDIF検出
Parameters
----------
response : np.ndarray
項目への応答(0/1)
group : np.ndarray
集団(0: 参照群, 1: 焦点群)
total_score : np.ndarray
総得点(能力の代理指標)
Returns
-------
dict
Uniform DIF検定結果、Non-uniform DIF検定結果
"""
df = pd.DataFrame({
'response': response,
'group': group,
'score': total_score,
'interaction': group * total_score
})
# Model 1: 能力のみ
model1 = logit('response ~ score', data=df).fit(disp=0)
# Model 2: 能力 + 集団(Uniform DIFの検定)
model2 = logit('response ~ score + group', data=df).fit(disp=0)
# Model 3: 能力 + 集団 + 交互作用(Non-uniform DIFの検定)
model3 = logit('response ~ score + group + score:group', data=df).fit(disp=0)
# Uniform DIF: Model 1 vs Model 2
lr_uniform = -2 * (model1.llf - model2.llf)
p_uniform = 1 - stats.chi2.cdf(lr_uniform, df=1)
# Non-uniform DIF: Model 2 vs Model 3
lr_nonuniform = -2 * (model2.llf - model3.llf)
p_nonuniform = 1 - stats.chi2.cdf(lr_nonuniform, df=1)
# Total DIF: Model 1 vs Model 3
lr_total = -2 * (model1.llf - model3.llf)
p_total = 1 - stats.chi2.cdf(lr_total, df=2)
return {
'uniform_dif': {
'chi2': lr_uniform,
'p_value': p_uniform,
'coefficient': model2.params.get('group', np.nan)
},
'nonuniform_dif': {
'chi2': lr_nonuniform,
'p_value': p_nonuniform,
'coefficient': model3.params.get('score:group', np.nan)
},
'total_dif': {
'chi2': lr_total,
'p_value': p_total
}
}シミュレーションデータによる検証¶
def simulate_dif_data(
n_reference: int = 500,
n_focal: int = 500,
n_items: int = 20,
dif_items: list[int] = None,
dif_magnitude: float = 0.5,
seed: int = 42
):
"""
DIFを含むシミュレーションデータを生成
Parameters
----------
n_reference : int
参照群の人数
n_focal : int
焦点群の人数
n_items : int
項目数
dif_items : list[int]
DIFを持つ項目のインデックス
dif_magnitude : float
DIFの大きさ(困難度パラメータの差)
seed : int
乱数シード
Returns
-------
tuple
応答データ、集団、総得点、真のDIF項目
"""
np.random.seed(seed)
if dif_items is None:
dif_items = [0, 1] # 最初の2項目にDIFを設定
n_total = n_reference + n_focal
# 能力パラメータ(両群とも標準正規分布)
theta = np.concatenate([
np.random.normal(0, 1, n_reference),
np.random.normal(0, 1, n_focal)
])
# 集団
group = np.concatenate([np.zeros(n_reference), np.ones(n_focal)]).astype(int)
# 項目パラメータ(1PLMを仮定)
b = np.random.uniform(-2, 2, n_items)
# 応答データの生成
responses = np.zeros((n_total, n_items))
for j in range(n_items):
b_j = b[j]
# DIF項目の場合、焦点群に対して困難度を変更
if j in dif_items:
b_effective = np.where(group == 1, b_j + dif_magnitude, b_j)
else:
b_effective = b_j
# 正答確率(1PLM)
prob = 1 / (1 + np.exp(-(theta - b_effective)))
responses[:, j] = (np.random.random(n_total) < prob).astype(int)
# 総得点
total_score = responses.sum(axis=1)
return responses, group, total_score, dif_items# シミュレーションデータの生成
responses, group, total_score, true_dif_items = simulate_dif_data(
n_reference=500,
n_focal=500,
n_items=20,
dif_items=[0, 1],
dif_magnitude=0.8
)
print(f"データサイズ: {responses.shape}")
print(f"参照群: {np.sum(group == 0)}, 焦点群: {np.sum(group == 1)}")
print(f"真のDIF項目: {true_dif_items}")# 各項目についてDIF検出を実行
results = []
for j in range(responses.shape[1]):
# 検討項目を除いた総得点を計算
score_without_item = total_score - responses[:, j]
mh_result = mantel_haenszel_dif(responses[:, j], group, score_without_item)
lr_result = logistic_regression_dif(responses[:, j], group, score_without_item)
results.append({
'item': j + 1,
'true_dif': j in true_dif_items,
'mh_chi2': mh_result['chi2_mh'],
'mh_p': mh_result['p_value'],
'mh_d_dif': mh_result['mh_d_dif'],
'lr_uniform_p': lr_result['uniform_dif']['p_value'],
'lr_nonuniform_p': lr_result['nonuniform_dif']['p_value'],
'lr_total_p': lr_result['total_dif']['p_value']
})
results_df = pd.DataFrame(results)
results_df# DIFが検出された項目
alpha = 0.05
print("=== Mantel-Haenszel法で検出されたDIF項目 ===")
detected_mh = results_df[results_df['mh_p'] < alpha]['item'].tolist()
print(f"検出された項目: {detected_mh}")
print(f"真のDIF項目: {[i+1 for i in true_dif_items]}")
print("\n=== ロジスティック回帰法で検出されたDIF項目 ===")
detected_lr = results_df[results_df['lr_total_p'] < alpha]['item'].tolist()
print(f"検出された項目: {detected_lr}")
print(f"真のDIF項目: {[i+1 for i in true_dif_items]}")DIFの解釈と対処¶
DIFが検出された場合、以下のステップで対処する:
統計的有意性の確認: 多重比較の補正(Bonferroni補正など)を考慮
効果量の評価: MH D-DIFの絶対値が1.0以上で「中程度」、1.5以上で「大きい」とされる(ETS基準)
項目内容の精査: 専門家による項目レビューを実施し、DIFの原因を特定
対処の決定:
項目の削除
項目の修正
集団別の項目パラメータを使用
DIFの影響を考慮したスコアリング
DIFを考慮したスコアリング¶
DIFが検出された場合、従来の単純な総得点(素点)は集団間の公平な比較を歪める可能性がある。ここでは、DIFの影響を適切に扱うスコアリング手法を紹介する。
アプローチ1: DIF項目の除外¶
最も単純な方法は、DIFが検出された項目を除外して得点を算出することである。
ここで はDIF項目の集合である。この方法は直感的で実装が容易だが、項目数が減ることでテストの信頼性・測定精度が低下するという欠点がある。
アプローチ2: 集団別IRTパラメータによるスコアリング¶
DIF項目については集団ごとに異なるパラメータを用い、非DIF項目については共通パラメータを用いて能力値を推定する方法である。2PLMの場合、受験者 が集団 に属するとき、各項目 の正答確率は:
非DIF項目: (共通パラメータ)
DIF項目: (集団固有パラメータ)
能力値の推定には、集団に応じた適切なパラメータセットを用いてMLE(最尤推定)やEAP(期待事後推定)を行う。
この方法は全項目の情報を活用でき、DIFの影響を適切にモデル化できる。
アプローチ3: 期待得点調整¶
Dorans & Holland (1993) に基づく方法で、DIF項目について参照群のパラメータで期待される得点に置き換えることで、焦点群に対するDIFの影響を補正する。
焦点群の受験者 について、DIF項目 の調整済みスコアは:
すなわち、焦点群パラメータでの期待得点と参照群パラメータでの期待得点の差だけ調整を加える。テスト全体の調整済み得点は:
この方法は項目を除外せずにDIFの影響のみを除去でき、かつ非DIF項目からの情報を保持できる。
アプローチ4: 多母集団IRTモデル¶
最も理論的に洗練されたアプローチは、多母集団(multiple-group)IRTモデルを用いて全集団のデータを同時に推定することである。
このモデルでは:
アンカー項目(非DIF項目):全集団で共通の項目パラメータを推定
DIF項目:集団ごとに固有の項目パラメータを推定
能力分布:参照群を に固定し、焦点群の能力分布 を推定
全集団のデータを同時に用いるため推定精度が高く、等化が不要で、DIFの有無に関わらずすべての項目を活用できる。
実装例: 各スコアリング手法の比較¶
先のシミュレーションデータを用いて、各スコアリング手法の効果を比較する。
from scipy.optimize import minimize_scalar
def irt_1pl_prob(theta: np.ndarray, b: float) -> np.ndarray:
"""1PLMの正答確率"""
return 1 / (1 + np.exp(-(theta - b)))
def estimate_item_params_1pl(responses: np.ndarray, group: np.ndarray, dif_items: list[int]):
"""
集団別の項目パラメータを推定(1PLM、簡易版)
DIF項目は集団ごとに困難度を推定し、非DIF項目は全データで推定する。
Parameters
----------
responses : np.ndarray
応答行列 (n_persons x n_items)
group : np.ndarray
集団ラベル (0: 参照群, 1: 焦点群)
dif_items : list[int]
DIF項目のインデックス
Returns
-------
dict
集団ごとの困難度パラメータ
"""
n_items = responses.shape[1]
b_reference = np.zeros(n_items)
b_focal = np.zeros(n_items)
for j in range(n_items):
if j in dif_items:
# DIF項目: 集団別に推定
p_r = responses[group == 0, j].mean()
p_f = responses[group == 1, j].mean()
# ロジット変換で困難度を近似
b_reference[j] = -np.log(p_r / (1 - p_r)) if 0 < p_r < 1 else 0
b_focal[j] = -np.log(p_f / (1 - p_f)) if 0 < p_f < 1 else 0
else:
# 非DIF項目: 全データで推定
p_all = responses[:, j].mean()
b_common = -np.log(p_all / (1 - p_all)) if 0 < p_all < 1 else 0
b_reference[j] = b_common
b_focal[j] = b_common
return {'reference': b_reference, 'focal': b_focal}
def estimate_theta_mle(response_vector: np.ndarray, b: np.ndarray) -> float:
"""
1PLMのMLEによる能力推定
Parameters
----------
response_vector : np.ndarray
個人の応答パターン (n_items,)
b : np.ndarray
困難度パラメータ (n_items,)
Returns
-------
float
推定能力値
"""
def neg_log_lik(theta):
p = irt_1pl_prob(theta, b)
p = np.clip(p, 1e-10, 1 - 1e-10)
return -np.sum(response_vector * np.log(p) + (1 - response_vector) * np.log(1 - p))
result = minimize_scalar(neg_log_lik, bounds=(-4, 4), method='bounded')
return result.x# 項目パラメータの推定
detected_dif_items = [i - 1 for i in detected_mh] # MH法で検出されたDIF項目(0-indexed)
item_params = estimate_item_params_1pl(responses, group, detected_dif_items)
n_persons = responses.shape[0]
n_items = responses.shape[1]
# --- 方法0: 素点(ベースライン)---
raw_scores = total_score
# --- 方法1: DIF項目除外 ---
non_dif_mask = [j for j in range(n_items) if j not in detected_dif_items]
purified_scores = responses[:, non_dif_mask].sum(axis=1)
# --- 方法2: 集団別IRTパラメータによるMLE推定 ---
theta_group_specific = np.zeros(n_persons)
for i in range(n_persons):
b = item_params['reference'] if group[i] == 0 else item_params['focal']
theta_group_specific[i] = estimate_theta_mle(responses[i], b)
# --- 方法3: 期待得点調整 ---
# まず共通パラメータでθを推定
b_common = np.zeros(n_items)
for j in range(n_items):
p_all = responses[:, j].mean()
b_common[j] = -np.log(p_all / (1 - p_all)) if 0 < p_all < 1 else 0
theta_common = np.array([estimate_theta_mle(responses[i], b_common) for i in range(n_persons)])
# 焦点群のDIF項目について調整
adjusted_scores = total_score.copy().astype(float)
for j in detected_dif_items:
focal_mask = group == 1
p_focal = irt_1pl_prob(theta_common[focal_mask], item_params['focal'][j])
p_reference = irt_1pl_prob(theta_common[focal_mask], item_params['reference'][j])
adjusted_scores[focal_mask] += (p_reference - p_focal)
print(f"検出されたDIF項目: {[i+1 for i in detected_dif_items]}")
print(f"項目数: 全{n_items}項目, DIF除外後{len(non_dif_mask)}項目")import matplotlib.pyplot as plt
fig, axes = plt.subplots(2, 2, figsize=(12, 10))
# 真の能力値を再構成
np.random.seed(42)
theta_true = np.concatenate([
np.random.normal(0, 1, 500),
np.random.normal(0, 1, 500)
])
scoring_methods = [
("Raw Score (素点)", raw_scores),
("Purified Score (DIF項目除外)", purified_scores),
("Group-Specific IRT (集団別IRT)", theta_group_specific),
("Adjusted Score (期待得点調整)", adjusted_scores),
]
for ax, (title, scores) in zip(axes.flat, scoring_methods):
ax.scatter(theta_true[group == 0], scores[group == 0], alpha=0.3, s=10, label='Reference')
ax.scatter(theta_true[group == 1], scores[group == 1], alpha=0.3, s=10, label='Focal')
ax.set_xlabel('True θ')
ax.set_ylabel('Score')
ax.set_title(title)
ax.legend()
fig.suptitle('各スコアリング手法と真の能力値の関係', fontsize=14, y=1.01)
plt.tight_layout()
plt.show()# 各手法の評価: 集団間の得点差(同じ能力を持つ場合の公平性)
print("=== 各スコアリング手法の集団間比較 ===\n")
for title, scores in scoring_methods:
mean_r = scores[group == 0].mean()
mean_f = scores[group == 1].mean()
corr_r = np.corrcoef(theta_true[group == 0], scores[group == 0])[0, 1]
corr_f = np.corrcoef(theta_true[group == 1], scores[group == 1])[0, 1]
print(f"--- {title} ---")
print(f" 平均得点: 参照群={mean_r:.3f}, 焦点群={mean_f:.3f}, 差={mean_r - mean_f:.3f}")
print(f" θとの相関: 参照群={corr_r:.3f}, 焦点群={corr_f:.3f}")
print()手法の選択指針¶
| 手法 | 利点 | 欠点 | 推奨場面 |
|---|---|---|---|
| DIF項目除外 | 実装が容易 | 情報損失、信頼性低下 | DIF項目が少数の場合 |
| 集団別IRTパラメータ | 全項目の情報を活用 | 等化が必要、推定が複雑 | IRTモデルが適合する場合 |
| 期待得点調整 | 素点スケールで調整可能 | 能力推定の精度に依存 | 素点での報告が必要な場合 |
| 多母集団IRT | 理論的に最も適切 | 実装が複雑、大標本が必要 | 大規模テスト |
実務的には、DIFが少数の項目にのみ見られ効果量が小さい場合には素点のままでも許容される場合がある。一方、DIFが多くの項目に見られるか効果量が大きい場合には、集団別IRTパラメータや多母集団IRTモデルの利用を検討すべきである。
参考文献¶
Holland, P. W., & Wainer, H. (Eds.). (1993). Differential item functioning. Lawrence Erlbaum Associates.
DIFに関する包括的な教科書
Magis, D., Béland, S., Tuerlinckx, F., & De Boeck, P. (2010). A general framework and an R package for the detection of dichotomous differential item functioning. Behavior Research Methods, 42(3), 847-862.
RのdifRパッケージの論文。DIF検出手法の比較も行っている
Swaminathan, H., & Rogers, H. J. (1990). Detecting differential item functioning using logistic regression procedures. Journal of Educational Measurement, 27(4), 361-370.
ロジスティック回帰法によるDIF検出の基礎論文
- Magis, D., Béland, S., Tuerlinckx, F., & De Boeck, P. (2010). A general framework and an R package for the detection of dichotomous differential item functioning. Behavior Research Methods, 42(3), 847–862. 10.3758/brm.42.3.847