第12講 分散分析

複数のグループ間の違いを検証する

Published

April 6, 2026

準備

以下で利用する共通パッケージを読み込む.

library(conflicted)  # 関数名の衝突を警告
conflicts_prefer(    # 優先的に使う関数を指定
    dplyr::filter(),
    dplyr::select(),
    dplyr::lag(),
    )
library(tidyverse)
#' 日本語を用いるので macOS ではフォントの設定を行う
if(Sys.info()["sysname"] == "Darwin") { # macOS か調べて日本語フォントを指定
    theme_update(text = element_text(family = "HiraginoSans-W4"))
    update_geom_defaults("text", list(family = theme_get()$text$family))
    update_geom_defaults("label", list(family = theme_get()$text$family))}

一元配置分散分析

問題

一元配置分散分析について確率シミュレーションを行いなさい.

解答欄

解答例

人工データにより一元配置分散分析の Monte-Carlo 実験を行う.

A,B,Cの3つの水準をもつデータを考える.

fact <- as.factor(rep(LETTERS[1:3], c(10,8,12))) # 因子(水準A,B,Cが10,8,12個)
toy_data0 <- # 平均が全て等しい場合 (帰無仮説が正しい例)
    tibble(factor = fact,                                 # 水準
           value = rnorm(length(fact), mean = 3, sd = 2)) # 観測値

データの状況を図示する.

toy_data0 |>
    ggplot(aes(x = factor, y = value)) +
    geom_boxplot(fill = "pink") +
    geom_jitter(colour = "red", width = 0.2) +
    labs(title = "帰無仮説が正しい場合")

水準ごとの平均にずれのある対立仮説の例を作成する.

alt <- rep(c(1,-1,1), c(10,8,12)) # 水準ごとの平均のずれ
toy_data1 <- # 平均が水準ごとに異なる場合(対立仮説が正しい例)
    tibble(factor = fact,                                       # 水準
           value = rnorm(length(fact), mean = 3, sd = 2) + alt) # 観測値

同様に図示する.

toy_data1 |>
    ggplot(aes(x = factor, y = value)) +
    geom_boxplot(fill = "lightblue") +
    geom_jitter(colour = "blue", width = 0.2) +
    labs(title = "帰無仮説が誤りの場合")

これらの2つのデータに対して分散分析を行う. 帰無仮説が正しい場合は以下のようになる.

aov(value ~ factor, data = toy_data0) 
Call:
   aov(formula = value ~ factor, data = toy_data0)

Terms:
                  factor Residuals
Sum of Squares   1.21898  94.95877
Deg. of Freedom        2        27

Residual standard error: 1.875364
Estimated effects may be unbalanced
aov(value ~ factor, data = toy_data0) |>
    broom::tidy() # 分散分析表
# A tibble: 2 × 6
  term         df sumsq meansq statistic p.value
  <chr>     <dbl> <dbl>  <dbl>     <dbl>   <dbl>
1 factor        2  1.22  0.609     0.173   0.842
2 Residuals    27 95.0   3.52     NA      NA    

帰無仮説が誤り,すなわち対立仮説が正しい場合は以下のようになる.

aov(value ~ factor, data = toy_data1) 
Call:
   aov(formula = value ~ factor, data = toy_data1)

Terms:
                  factor Residuals
Sum of Squares  40.98323  59.51545
Deg. of Freedom        2        27

Residual standard error: 1.48468
Estimated effects may be unbalanced
aov(value ~ factor, data = toy_data1) |>
    broom::tidy() # 分散分析表
# A tibble: 2 × 6
  term         df sumsq meansq statistic   p.value
  <chr>     <dbl> <dbl>  <dbl>     <dbl>     <dbl>
1 factor        2  41.0  20.5       9.30  0.000848
2 Residuals    27  59.5   2.20     NA    NA       
Note

関数 aov() のようにデータが第1引数でない場合にパイプ演算子を使うには _ (placeholder) を使って位置を明示すればよい.

toy_data0 |> aov(value ~ factor, data = _) 
Call:
   aov(formula = value ~ factor, data = toy_data0)

Terms:
                  factor Residuals
Sum of Squares   1.21898  94.95877
Deg. of Freedom        2        27

Residual standard error: 1.875364
Estimated effects may be unbalanced
toy_data0 |>
    aov(value ~ factor, data = _) |>
    broom::tidy()
# A tibble: 2 × 6
  term         df sumsq meansq statistic p.value
  <chr>     <dbl> <dbl>  <dbl>     <dbl>   <dbl>
1 factor        2  1.22  0.609     0.173   0.842
2 Residuals    27 95.0   3.52     NA      NA    

Monte-Carlo 実験を行う. まず,試行のための関数を作成する.

mc_trial <- function(h0 = TRUE){
    if(h0) { # 帰無仮説が正しい場合
        tmp <-
            tibble(factor = fact,
                   value = rnorm(length(fact), mean = 3, sd = 2)) |>
            aov(value ~ factor, data = _) |> broom::tidy()
    } else { # 対立仮説が正しい場合
        tmp <- 
            tibble(factor = fact, # 水準ごとに平均が異なる
                   value = rnorm(length(fact), mean = 3, sd = 2) + alt) |>
            aov(value ~ factor, data = _) |> broom::tidy()
    }
    #' 分散分析表における factor 行の統計量とp値を返す
    return(c(statistic = tmp[1,5, drop = TRUE],
             p.value   = tmp[1,6, drop = TRUE]))
    #' 以下のように書いても良い
    #' return(tmp |> select(5:6) |> t() |> _[,1])
}

帰無仮説が正しい場合のF統計量およびp値の分布を調べる. この場合,p値は一様分布になることに注意する.

replicate(2000, mc_trial()) |>
    t() |> as_tibble() |> pivot_longer(everything()) |> # 描画用に変形
    ggplot(aes(x = value, fill = name)) +
    geom_histogram(show.legend = FALSE) +
    facet_wrap(vars(name), ncol = 2, scales = "free") + # それぞれ個別の座標で描画
    labs(title = "帰無仮説が正しい場合")

対立仮説が正しい場合のF統計量およびp値の分布を調べる. この場合,p値は小さな値に偏ることが確認できる.

replicate(2000, mc_trial(h0 = FALSE)) |>
    t() |> as_tibble() |> pivot_longer(everything()) |> 
    ggplot(aes(x = value, fill = name)) +
    geom_histogram(show.legend = FALSE) +
    facet_wrap(vars(name), ncol = 2, scales = "free") + 
    labs(title = "帰無仮説が誤りの場合")

問題

東京の気候データ (tokyo_weather.csv) の 気温の項目について以下の問に答えよ.

  • 曜日ごとの気温の平均と分散を求めよ.
  • 曜日ごとに平均が異なるといえるかどうか分散分析を用いて検定しなさい.
Note

週日を因子(factor)とするには例えば以下のような項目を加えればよい.

tw_data <- read_csv("data/tokyo_weather.csv") |>
    mutate(day_of_week = ordered(day_of_week, # 曜日を順序付き因子にする
                                 levels = wday(1:7, label = TRUE))) 

関数 lubridate::wday() の出力は使用言語に依存するので, 強制的に英語にするには locale を指定して wday(..., locale = "en_US") とすれば良い. 省略形を用いない場合は abbr を指定する. 以下はドイツ語の例.

wday(1:7, label = TRUE, abbr = FALSE, locale = "de_DE")

利用可能な言語を調べるには stringi::stri_locale_list() を用いる.

解答欄

解答例

気候データにおいて 曜日ごとの気温に差があるか否かを分散分析を用いて調べる.

データを読み込み,整理する.

tw_data <- read_csv("data/tokyo_weather.csv") |>
    mutate(day_of_week = ordered(day_of_week, # 曜日を順序付き因子にする
                                 levels = wday(1:7, label = TRUE))) 

曜日ごとの分布を見るとともに平均・分散(標準偏差)を確認する.

tw_data |>
    ggplot(aes(x = day_of_week, y = temp)) +
    geom_boxplot(fill = "lavender") +
    labs(x = "曜日", y = "気温", title = "曜日ごとの気温の分布") 

tw_data |>
    group_by(day_of_week) |>
    summarize(across(temp, list(mean = mean,
                                var = var,
                                sd = sd)))
# A tibble: 7 × 4
  day_of_week temp_mean temp_var temp_sd
  <ord>           <dbl>    <dbl>   <dbl>
1 Sun              17.6     78.0    8.83
2 Mon              17.8     75.2    8.67
3 Tue              17.4     79.3    8.91
4 Wed              17.2     79.1    8.89
5 Thu              17.5     71.0    8.43
6 Fri              17.1     65.8    8.11
7 Sat              16.9     76.7    8.76

曜日ごとの気温差に関する分散分析を行う.

aov(temp ~ day_of_week, data = tw_data) # aovによる分析
Call:
   aov(formula = temp ~ day_of_week, data = tw_data)

Terms:
                day_of_week Residuals
Sum of Squares       29.139 26859.345
Deg. of Freedom           6       358

Residual standard error: 8.661761
Estimated effects may be unbalanced
aov(temp ~ day_of_week, data = tw_data) |>
    summary() # 分散分析表の表示(棄却されない)
             Df Sum Sq Mean Sq F value Pr(>F)
day_of_week   6     29    4.86   0.065  0.999
Residuals   358  26859   75.03               
aov(temp ~ day_of_week, data = tw_data) |>
    broom::tidy() # 分散分析表のデータフレーム (行・列の番号で必要な情報が取得できる)
# A tibble: 2 × 6
  term           df   sumsq meansq statistic p.value
  <chr>       <dbl>   <dbl>  <dbl>     <dbl>   <dbl>
1 day_of_week     6    29.1   4.86    0.0647   0.999
2 Residuals     358 26859.   75.0    NA       NA    
aov(temp ~ day_of_week, data = tw_data) |>
    model.tables(type = "means")   # 水準(曜日)ごとの平均値
Tables of means
Grand mean
         
17.36877 

 day_of_week 
      Sun   Mon   Tue   Wed   Thu   Fri  Sat
    17.62 17.77 17.42 17.23 17.54 17.11 16.9
rep 52.00 52.00 52.00 53.00 52.00 52.00 52.0
aov(temp ~ day_of_week, data = tw_data) |>
    model.tables(type = "effects") # 水準(曜日)ごとの効果
Tables of effects

 day_of_week 
        Sun     Mon      Tue     Wed     Thu     Fri     Sat
     0.2466  0.3985  0.05046 -0.1424  0.1755 -0.2553 -0.4707
rep 52.0000 52.0000 52.00000 53.0000 52.0000 52.0000 52.0000

検定のみ実行する場合は,関数 oneway.test() を用いることができる.

級内の分散が等しいことを仮定する場合は以下のように指定する.

oneway.test(temp ~ day_of_week, data = tw_data, var.equal = TRUE) 

    One-way analysis of means

data:  temp and day_of_week
F = 0.064732, num df = 6, denom df = 358, p-value = 0.9989

級内の分散が等しいことが仮定できない場合はWelchの近似法による検定が行われる.

oneway.test(temp ~ day_of_week, data = tw_data) 

    One-way analysis of means (not assuming equal variances)

data:  temp and day_of_week
F = 0.063645, num df = 6.00, denom df = 159.09, p-value = 0.999

月ごとの気温に差があるか否かを分散分析で検証する. 図示した結果から棄却されることが予想される.

tw_data <- tw_data |> mutate(month = as_factor(month)) # 月を因子化
tw_data |>
    ggplot(aes(x = month, y = temp)) +
    geom_boxplot(fill = "lavender") +
    labs(x = "月", y = "気温", title = "月ごとの気温の分布") 

aov(temp ~ month, data = tw_data) |>
    summary() # 分散分析表の表示(棄却される)
             Df Sum Sq Mean Sq F value Pr(>F)    
month        11  24132  2193.8   280.9 <2e-16 ***
Residuals   353   2757     7.8                   
---
Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
Note

曜日の因子化などは package::lubridate に含まれる関数が利用できる.

曜日の文字列を得る方法には以下のものがある.

wday(1:7, label = TRUE) # 省略名
[1] Sun Mon Tue Wed Thu Fri Sat
Levels: Sun < Mon < Tue < Wed < Thu < Fri < Sat
wday(1:7, label = TRUE, abbr = FALSE) # 正式名
[1] Sunday    Monday    Tuesday   Wednesday Thursday  Friday    Saturday 
7 Levels: Sunday < Monday < Tuesday < Wednesday < Thursday < ... < Saturday
wday(2:8, label = TRUE) # 月曜から始まる文字列を得る場合
[1] Mon  Tue  Wed  Thu  Fri  Sat  <NA>
Levels: Sun < Mon < Tue < Wed < Thu < Fri < Sat
wday(1:7, label = TRUE, week_start = 1) # 順序を月曜から始める場合
[1] Sun Mon Tue Wed Thu Fri Sat
Levels: Mon < Tue < Wed < Thu < Fri < Sat < Sun

日付から曜日を得る方法は例えば以下のようにすれば良い.

tw_data |>
    mutate(date = as_date(paste(year,month,day, sep = "-")), # 日付に変換
           weekday = wday(date, label = TRUE), # 日付から曜日(順序付因子)を追加
           .before = year) # year列の前に付加
# A tibble: 365 × 15
   date       weekday  year month   day day_of_week  temp  rain solar  snow
   <date>     <ord>   <dbl> <fct> <dbl> <ord>       <dbl> <dbl> <dbl> <dbl>
 1 2025-01-01 Wed      2025 1         1 Wed           7.1     0 11.4      0
 2 2025-01-02 Thu      2025 1         2 Thu           7.8     0 11.2      0
 3 2025-01-03 Fri      2025 1         3 Fri           5.5     0  3.97     0
 4 2025-01-04 Sat      2025 1         4 Sat           5.1     0 11.8      0
 5 2025-01-05 Sun      2025 1         5 Sun           4.6     0 10.4      0
 6 2025-01-06 Mon      2025 1         6 Mon           4.5    25  4.26     0
 7 2025-01-07 Tue      2025 1         7 Tue           8.1     0  7.34     0
 8 2025-01-08 Wed      2025 1         8 Wed           6.6     0 10.1      0
 9 2025-01-09 Thu      2025 1         9 Thu           6.7     0 10.8      0
10 2025-01-10 Fri      2025 1        10 Fri           4.6     0 12.5      0
# ℹ 355 more rows
# ℹ 5 more variables: wdir <chr>, wind <dbl>, press <dbl>, humid <dbl>,
#   cloud <dbl>

二元配置分散分析

問題

package::datarium に含まれている jobsatisfaction データ (性別 (gender) と 学歴 (education_level) の違いによる 仕事の満足度 (score) を収集したデータ) について以下の問に答えよ.

  • データを適当な方法で可視化しなさい.
  • それぞれの因子で満足度の平均に違いがあるか,二元配置の分散分析を用いて検討しなさい.
Note

パッケージのインストールと読み込みは以下のように行うことができる.

install.packages("datarium") # インストール
library("datarium") # 読み込み

解答欄

解答例

datarium::jobsatisfaction は 性別(gender)と学歴(education_level)による仕事の満足度(score)を集計したデータである. 二元配置分散分析を用いて満足度にそれぞれの因子の効果があるかを検証する.

必要であれば package::datarium をインストールし, パッケージの読み込みを行う.

#' install.packages("datarium") # 検定用のデータが集められている
library("datarium") # packageの読み込み
jobsatisfaction     # データの内容を確認
# A tibble: 58 × 4
   id    gender education_level score
   <fct> <fct>  <fct>           <dbl>
 1 1     male   school           5.51
 2 2     male   school           5.65
 3 3     male   school           5.07
 4 4     male   school           5.51
 5 5     male   school           5.94
 6 6     male   school           5.8 
 7 7     male   school           5.22
 8 8     male   school           5.36
 9 9     male   school           4.78
10 10    male   college          6.01
# ℹ 48 more rows

データの視覚化を行う.

jobsatisfaction |> # 学歴ごとに性別を比較
    ggplot(aes(x = education_level, y = score, fill = gender)) +
    geom_boxplot()

jobsatisfaction |> # 性別ごとに学歴を比較
    ggplot(aes(x = gender, y = score, fill = education_level)) +
    geom_boxplot()

二元配置分散分析を行い,結果をいくつかの方法で表示する.

aov(score ~ gender + education_level, data=jobsatisfaction) |>
    summary()
                Df Sum Sq Mean Sq F value Pr(>F)    
gender           1   0.54    0.54   1.447  0.234    
education_level  2 113.68   56.84 152.172 <2e-16 ***
Residuals       54  20.17    0.37                   
---
Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
aov(score ~ gender + education_level, data=jobsatisfaction) |>
    model.tables(type = "means")  
Tables of means
Grand mean
         
6.963276 

 gender 
      male female
     7.063   6.87
rep 28.000  30.00

 education_level 
    school college university
     5.594   6.351      8.846
rep 19.000  19.000     20.000
aov(score ~ gender + education_level, data=jobsatisfaction) |>
    model.tables(type = "effects")
Tables of effects

 gender 
        male   female
     0.09994 -0.09328
rep 28.00000 30.00000

 education_level 
    school college university
    -1.369  -0.612      1.882
rep 19.000  19.000     20.000