Rの関数qmvt {mvtnorm}を利用したガブリエル比較区間

Rの関数 qmvt {mvtnorm} を利用して、ガブリエル比較区間を算出します。

本ポストはこちらの続きです。

Rの関数:qmvt {mvtnorm}
const typesetMath = (el) => { if (window.MathJax) { // MathJax Typeset window.MathJax.typeset(); } else if (window.katex...

ガブリエル比較区間とは

多重比較(ANOVA後の検定など)において、「どの群とどの群に差があるか」を視覚的に判定するための手法です。

  • 通常の95%信頼区間:

    • 区間が重なっていても、有意差がある場合がある(「重なり」=「差がない」とは限らない)。
  • ガブリエル区間:

    • 「区間が重ならない \(\iff\) 統計的に有意差がある」 となるように設計された区間。

この区間の計算には、スチューデント化最大絶対値分布(SMM: Studentized Maximum Modulus distribution)の分位点が必要であり、ここで qmvt を利用します。

シミュレーションの設計

  1. データ生成:

    • 3つのグループ(A, B, C)を作成します。
    • グループAとBの間には、「通常の信頼区間なら重なって見えるが、実際には有意差がある」という微妙な差を設定します。
  2. qmvtの利用:

    • SMM分布のクリティカル・バリューを qmvt で計算します(相関行列を単位行列に設定することでSMMを再現できます)。
  3. 可視化:

    • 通常の信頼区間(重なる)と、ガブリエル区間(重ならない)を並べてプロットし、判定のしやすさを比較します。

ガブリエル比較区間のシミュレーション

# パッケージの読み込み
library(mvtnorm)
library(ggplot2)
library(dplyr)

# 乱数シードの固定
seed <- 20260108
set.seed(seed)

# 1. サンプルデータの生成
# 3つのグループを作成
# グループAとBの差は「標準誤差の約3.7倍」に設定
# (通常の95%CIは重なるが、有意差はある絶妙な距離)
n_per_group <- 30
sd_val <- 5
se_expected <- sd_val / sqrt(n_per_group) # 約 0.91

# 平均値の設定
mean_A <- 10
mean_B <- 10 + (3.7 * se_expected) # Aとの差は約3.4
mean_C <- 10 - (1.0 * se_expected) # Aと差がない

cat("--- サンプルデータの設計 ---\n")
cat(sprintf("設定された真の平均値: A=%.2f, B=%.2f, C=%.2f\n", mean_A, mean_B, mean_C))
cat(sprintf("期待される標準誤差(SE): %.2f\n\n", se_expected))

df_sim <- data.frame(
  Group = rep(c("A", "B", "C"), each = n_per_group),
  Value = c(
    rnorm(n_per_group, mean = mean_A, sd = sd_val),
    rnorm(n_per_group, mean = mean_B, sd = sd_val),
    rnorm(n_per_group, mean = mean_C, sd = sd_val)
  )
)

# 2. 統計量の計算
# グループごとの平均、標準偏差、SE、自由度を計算
stats_df <- df_sim %>%
  group_by(Group) %>%
  summarise(
    Mean = mean(Value),
    SD = sd(Value),
    n = n(),
    SE = SD / sqrt(n),
    .groups = "drop"
  )

# 全体の自由度 (df)
total_df <- sum(stats_df$n) - nrow(stats_df)
k <- nrow(stats_df) # グループ数

cat("--- グループ別統計量 ---\n")
print(stats_df)
--- サンプルデータの設計 ---
設定された真の平均値: A=10.00, B=13.38, C=9.09
期待される標準誤差(SE): 0.91

--- グループ別統計量 ---
# A tibble: 3 × 5
  Group  Mean    SD     n    SE
  <chr> <dbl> <dbl> <int> <dbl>
1 A      9.81  5.63    30 1.03 
2 B     13.4   5.23    30 0.954
3 C      7.52  5.38    30 0.982

qmvt による SMMクリティカル値の計算

求める分位点(棄却限界値)は 両側95% とします。

# 3. クリティカル・バリューの計算 (qmvtの利用)

# (1) 通常のt分布のクリティカル値 (95%両側)
# Bonferroni補正なしの単純なCI用
qt_crit <- qt(0.975, df = total_df)

# (2) ガブリエル区間用のクリティカル値 (SMM分布)
# SMM分布は、相関行列が単位行列である多変量t分布と等価です。
# tail = "both.tails" で絶対値の最大値を考えます。
corr_mat <- diag(k) # 単位行列 (グループ間は独立とみなす)

qmvt_res <- qmvt(
  p = 0.95,
  df = total_df,
  corr = corr_mat,
  tail = "both.tails",
  seed = seed
)

qsmm_crit <- qmvt_res$quantile
cat("--- クリティカル値 ---\n")
cat(sprintf("通常のt値 (95%%): %.4f\n", qt_crit))
cat(sprintf("SMM値 (Gabriel用): %.4f\n\n", qsmm_crit))


# 4. 区間幅の計算
# ガブリエル区間の半径 = (SMM値 * SE) / sqrt(2)
# sqrt(2) で割ることで、区間の重なり判定が検定と一致するように調整される

plot_df <- stats_df %>%
  mutate(
    # 通常の95%信頼区間
    CI_Lower = Mean - qt_crit * SE,
    CI_Upper = Mean + qt_crit * SE,

    # ガブリエル比較区間
    Gab_Width = (qsmm_crit * SE) / sqrt(2),
    Gab_Lower = Mean - Gab_Width,
    Gab_Upper = Mean + Gab_Width
  )

cat("--- 算出された区間幅 ---\n")
print(plot_df %>% select(Group, CI_Lower, CI_Upper, Gab_Lower, Gab_Upper))


# 5. 可視化 (エラーバーの重なり比較)
# 見やすくするために、データを縦持ちに変換して描画
plot_data_long <- bind_rows(
  plot_df %>% mutate(Type = "通常の95%信頼区間", Lower = CI_Lower, Upper = CI_Upper),
  plot_df %>% mutate(Type = "ガブリエル比較区間", Lower = Gab_Lower, Upper = Gab_Upper)
)

p1 <- ggplot(plot_data_long, aes(x = Group, y = Mean, color = Type)) +
  # エラーバー
  geom_errorbar(aes(ymin = Lower, ymax = Upper),
    width = 0.2, size = 1.2, position = position_dodge(width = 0.5)
  ) +
  # 平均値の点
  geom_point(size = 4, position = position_dodge(width = 0.5)) +

  # AとBの境界線(比較用)
  geom_segment(
    aes(
      x = 0.8, xend = 2.2,
      y = plot_df$CI_Upper[plot_df$Group == "A"],
      yend = plot_df$CI_Upper[plot_df$Group == "A"]
    ),
    linetype = "dotted", color = "blue", alpha = 0.5
  ) +
  geom_segment(
    aes(
      x = 0.8, xend = 2.2,
      y = plot_df$Gab_Upper[plot_df$Group == "A"],
      yend = plot_df$Gab_Upper[plot_df$Group == "A"]
    ),
    linetype = "dotted", color = "red", alpha = 0.5
  ) +
  labs(
    title = "通常の信頼区間 vs ガブリエル比較区間",
    y = "値",
    color = "区間の種類"
  ) +
  scale_color_manual(values = c(
    "通常の95%信頼区間" = "steelblue",
    "ガブリエル比較区間" = "firebrick"
  )) +
  theme_minimal()

print(p1)
--- クリティカル値 ---
通常のt値 (95%): 1.9876
SMM値 (Gabriel用): 2.4327

--- 算出された区間幅 ---
# A tibble: 3 × 5
  Group CI_Lower CI_Upper Gab_Lower Gab_Upper
  <chr>    <dbl>    <dbl>     <dbl>     <dbl>
1 A         7.77    11.9       8.05     11.6 
2 B        11.6     15.3      11.8      15.1 
3 C         5.57     9.47      5.83      9.21
Figure 1

Figure 1 の『グループAの上限』と『グループBの下限』を確認しますと、通常の信頼区間(青)では Aの上限 > Bの下限 となっており、バーが重なっていますが、ガブリエル区間(赤)では Aの上限 < Bの下限 となっており、隙間があるため、『バーが重ならない=有意差あり』と視覚的に正しく判定することができます。

\(\sqrt{2}\) で割る理由

「統計的に有意」である条件

2つのグループ A と B の平均値(\(\bar{X}_A, \bar{X}_B\))に有意差があるかどうかを検定する場合、その差の標準誤差(\(SE_{diff}\))は以下のようになります(標準誤差 \(SE\) が同じと仮定)。

\[ SE_{diff} = \sqrt{SE^2 + SE^2} = \sqrt{2 SE^2} = \sqrt{2} \times SE \]

検定統計量 \(q\)(ここではSMM分布のクリティカル値)を使うと、有意差がある条件は以下のようになります。

\[ |\bar{X}_A - \bar{X}_B| > q \times (\sqrt{2} \times SE) \]

つまり、平均値の距離が \(\sqrt{2} \approx 1.414\) 倍以上 離れていれば「有意」です。

「見た目で重ならない」条件

一方、グラフ上でエラーバー(区間の片側の幅を \(w\) とします)を描いたとき、2つの区間が重ならない条件は単純な足し算です。

\[ |\bar{X}_A - \bar{X}_B| > w + w = 2w \]

つまり、平均値の距離が \(2\) 倍以上 離れていれば「重ならない」です。

ズレの調整(\(\sqrt{2}\)で割る理由)
  • 統計的な基準: 1.414 (\(=\sqrt{2}\)) 倍離れればOK
  • 見た目の基準: 2.0 倍離れないと隙間ができない

つまり、「実際は有意であるのに(1.5倍くらい離れている)、見た目では重なってしまう(2.0倍には届かない)」ことになります。

そこで、「見た目の基準(2倍)」が「統計的な基準(\(\sqrt{2}\)倍)」と等しくなるように、バーの長さ \(w\) を短く調整します。

\[ 2w = \sqrt{2} \times q \times SE \]

この式を変形して \(w\)(ガブリエル区間の幅)を求めます。

\[ w = \frac{\sqrt{2} \times q \times SE}{2} \]

ここで、分母の \(2\)\(\sqrt{2} \times \sqrt{2}\) ですので、約分できます。

\[ w = \frac{q \times SE}{\sqrt{2}} \]

rgabriel{rgabriel} との比較

ガブリエル比較区間を求めるための関数rgabriel{rgabriel}と結果を比較します。

# パッケージの読み込み
library(rgabriel)

cat("--- mvtnorm::qmvt を用いた手計算 ---\n")

# ガブリエル区間の半径 (Half-Width) の計算
# w = (q * SE) / sqrt(2)
q_smm_manual <- qmvt_res$quantile
stats_df$Manual_Radius <- (q_smm_manual * stats_df$SE) / sqrt(2)

cat(sprintf("SMMクリティカル値 (mvtnorm): %.4f\n", q_smm_manual))
cat("求められた半径:\n")
print(stats_df %>% select(Group, Manual_Radius))


cat("\n--- rgabriel::rgabriel を用いた計算 ---\n")

# rgabriel関数の実行
# 戻り値は各グループの「区間の半径」のベクトルです
pkg_radius <- rgabriel(df_sim$Value, df_sim$Group)

# 結果をデータフレームに結合
stats_df$Pkg_Radius <- pkg_radius

cat("求められた半径:\n")
print(stats_df %>% select(Group, Pkg_Radius))
cat("\n")

cat("\n--- 結果の比較 ---\n")
stats_df$Diff <- stats_df$Manual_Radius - stats_df$Pkg_Radius

print(stats_df %>% select(Group, Manual_Radius, Pkg_Radius, Diff))

# 視覚化による確認
p_comp <- ggplot(stats_df, aes(x = Group)) +
  geom_errorbar(aes(ymin = Mean - Manual_Radius, ymax = Mean + Manual_Radius),
    width = 0.2, color = "blue", size = 2, alpha = 0.5
  ) +
  geom_errorbar(aes(ymin = Mean - Pkg_Radius, ymax = Mean + Pkg_Radius),
    width = 0.2, color = "red", size = 0.5
  ) +
  geom_point(aes(y = Mean), size = 3) +
  labs(
    title = "計算結果の比較",
    subtitle = "青(太線):mvtnormによる手動計算\n赤(細線):rgabrielによる計算",
    y = "値"
  ) +
  theme_minimal()

print(p_comp)
--- mvtnorm::qmvt を用いた手計算 ---
SMMクリティカル値 (mvtnorm): 2.4327
求められた半径:
# A tibble: 3 × 2
  Group Manual_Radius
  <chr>         <dbl>
1 A              1.77
2 B              1.64
3 C              1.69

--- rgabriel::rgabriel を用いた計算 ---
求められた半径:
# A tibble: 3 × 2
  Group Pkg_Radius
  <chr>  <dbl[1d]>
1 A           1.77
2 B           1.64
3 C           1.69


--- 結果の比較 ---
# A tibble: 3 × 4
  Group Manual_Radius Pkg_Radius       Diff
  <chr>         <dbl>  <dbl[1d]>  <dbl[1d]>
1 A              1.77       1.77 -0.0000825
2 B              1.64       1.64 -0.0000766
3 C              1.69       1.69 -0.0000788
Figure 2

ほぼ、一致しています。

なお、SMM分布のクリティカル値計算アルゴリズムに以下の違いがあります。

  • mvtnorm : 準モンテカルロ法を用いた数値積分。
  • rgabriel : 独自の1次元数値積分(integrate関数)と近似ロジック。

以上です。