カイ二乗検定(独立性の検定)を Rust で実装してみましたのでメモを残します。
Rust 修行中の身ですので、あえて Excel や Python は使いません。
昔の統計の教科書を引っ張り出して復習しました。
理解が間違っている場合はご指摘下さい。
カイ二乗分布関数は mathru クレートを使いました。
また、CSV ファイルから二次元配列を取り出す部分は前回実装したものを使いました。
お題
一人暮らしをしているかどうかについてアンケートをとったところ、以下のような結果となったとします。
一人暮らし | 一人暮らしでない | |
---|---|---|
男性 | 53 | 32 |
女性 | 40 | 63 |
ここで一人暮らしかどうかが性別によって差があるかを調べたいとします。
- 独立性検定(カイ二乗検定)を用い、有意水準 5% で検定するものとします。
- データは CSVファイル (test.csv) に保存され、次のような内容ととします。
性別/一人暮らしか,一人暮らし,一人暮らしでない
男性,53,32
女性,40,63
実装
関数 independence_test は検定対象となるクロス集計表の入ったCSVファイルのパスを引数にとります。
最初に CSV ファイルを f32 の二次元配列として読み出します。
// 有意水準 = 5%
const ALPHA:f32 = 0.05;
use std::io::Result;
use std::io::Error;
use std::io::ErrorKind;
use mathru::statistics::distrib::ChiSquare;
use mathru::statistics::distrib::Continuous;
// カイ二乗検定(独立性検定)
fn independence_test (csv_file: &str) -> Result<()> {
// CSV ファイルを f32 の二次元配列で読み出し
let d:TwoDimentionalArray<f32> = from_csv_file(csv_file)?;
println!("データ:\n{}", d);
つづいてデータの整合性チェックを行います。
ここでは合計値の確認と、カイ二乗検定を行う条件を満たすことを確認します。
// データのチェック
// 各列の合計値の計算
let c_sums:Vec<f32> = (0..d.col_size()).map(|j|d.col(j).iter().sum()).collect();
//println!("sums of cols = {:?}", c_sums);
// 各行の合計値の計算
let r_sums:Vec<f32> = (0..d.row_size()).map(|i|d.row(i).iter().sum()).collect();
//println!("sums of rows = {:?}", r_sums);
// 全体の合計 (列の合計より)
let total:f32 = c_sums.iter().sum();
// 全体の合計 (行の合計より)
let total2:f32 = r_sums.iter().sum();
// 合計のチェック。合わないときはエラー
if total != total2 {
return Err(Error::new(ErrorKind::Other,
"totals of column and row are different!"))
}
// 条件1:セル数は20以下であること
if d.size() > 20 {
println!("データのセル数がカイ二乗検定の条件を満たさない(条件1)");
return Ok(());
}
println!("---------------");
次に期待度数表を作成し、それがカイ二乗検定の条件を満たすか確認します。
// 期待度数表の作成
let mut expected_dat:Vec<f32> = vec![];
let mut i: usize = 0;
while i<d.row_size() { // i 行
let mut j: usize = 0;
while j < d.col_size() { // j 列
expected_dat.push(r_sums[i]*c_sums[j]/total);
j += 1;
}
i += 1;
}
let expected: TwoDimentionalArray<f32> = TwoDimentionalArray::new(
d.col_size(), d.row_size(), expected_dat)?;
println!("期待度数表:\n{}", expected);
// 期待度数表のチェック
// 条件2:各セルの期待度数はクラス集計表内の80%で5以上であること
let counter:usize = expected.iter().map(|x|if *x>=5.0 {1} else {0}).sum();
if counter <= ((expected.size() as f32) * 0.8) as usize {
println!("期待度数がカイ二乗検定の条件を満たさない(条件2)");
return Ok(());
}
// 条件3:期待度数が1未満のセルがないこと
let counter:usize = expected.iter().map(|x|if *x<1.0 {1} else {0}).sum();
if counter > 0 {
println!("期待度数がカイ二乗検定の条件を満たさない(条件3)");
return Ok(());
}
次にカイ二乗統計量 $\chi^2$ と対象データの自由度を求め、カイ二乗分布の累積分布関数 (cdf) を用いて、カイ二乗統計量に対応する p 値を求めます。
// カイ二乗統計量の算出
let chi2:f32 = d.iter().zip(expected.iter()).map(|(d, e)|(d-e)*(d-e)/e).sum();
// 自由度の算出
let df = ((d.col_size() - 1) * (d.row_size() - 1)) as u32;
// 自由度 df のカイ二乗分布でカイ二乗統計量に対応する p 値を求める
let cs: ChiSquare<f32> = ChiSquare::new(df);
let p_value = 1.0-cs.cdf(chi2);
println!("---------------");
独立性検定では次の帰無仮説と対立仮説を設定します。
- 帰無仮説:二つの変数(性別と一人暮らしか)の間には関連性はない(独立している)
- 対立仮説:二つの変数(性別と一人暮らしか)の間には関連性がある(独立していない)
求めた p 値が有意水準(ALPHA)以下の場合には帰無仮説を偽として棄却し、対立仮説を採択します。
最後に検定の結果を表示します。
// 結果の表示
println! ("[検定結果] 二つの変数の間には{}。", if p_value <= ALPHA {
"関連性がある(独立していない)" // 対立仮説
} else {
"関連性はない(独立している)" // 帰無仮説
});
println!("---------------");
println!("自由度: {}", df);
println!("有意水準: {}", ALPHA);
println!("カイ二乗統計量: {}", chi2);
println!("p 値: {}", p_value);
Ok(())
}
実行結果
次の結果になりました。
データ:
[ 53.0 32.0
40.0 63.0 ]
---------------
期待度数表:
[ 42.04787 42.95213
50.95213 52.04787 ]
---------------
[検定結果] 二つの変数の間には関連性がある(独立していない)。
---------------
自由度: 1
有意水準: 0.05
カイ二乗統計量: 10.30405
p 値: 0.0013273954
一人暮らしかどうかと性別の間には関連性がある(独立していない)という結論が出ました。
感想など。
- カイ二乗検定が有効に使える条件を忘れてました。これまでやってた検定はたまたま条件を満たしていただけだったのかもしれません。
- カイ二乗統計量の計算など、使えるところは意識的にイテレータを使用しました。
- というか、一行で書けてしまうイテレータが便利すぎて、もう以前には戻れません。