pythonのprastクラスによる集計(3) カイ2乗検定

2018/09/26

 今回はクロス集計の次ということで、カイ2乗検定を取り上げます。

 処理するcsvファイルは第1回・第2回と同じ data01.csv です。

 当Webページで紹介するスクリプトや素材データ一式は、
prast03.zip という圧縮ファイルに同梱しておきます。

    


《このページの目次》


    

はじめに

 カイ2乗検定は、2つの事象の間に有意な関連性があるかどうかを検証するものです。

 検定の素材としてクロス集計表を与えます。

  賛成 反対 保留
男性 73 98 30
女性 78 96 18

 性別×意見のクロス集計表を与えた場合、
この2つの事象が独立である(互いに影響しあう訳ではない)
それとも関連性がある(影響しあう)のどちらなのかを見ます。

 要するに、男女で意見が違うかどうかをみます。

 「カイ」というのはギリシャ文字のχ(小文字)のことですが、
ここではカナで記します。

 χは、アルファベットでいうと x(エックス)に該当するようです。

 カイ2乗検定の結果、2つの事象に関連性があるということになった場合、
表の中のどのセルが有意に大きな値が(あるいは小さな値か)を見るのに
残差分析を行います。

 そこで、Prastクラスの chisq_test() では、残差関連の情報も返すようにしました。

目次に戻る


1. scipyライブラリの chi2_contingency() によるカイ2乗検定

 pythonのscipyライブラリには、カイ2乗検定を行うための
chi2_contingency() が用意されています。

 戻り値として統計量、有意確率、自由度、期待度数を得ることができます。

 ですが、残差に関する表が得られません。

    

 scipyに用意されているメソッドの呼び出し方は次のとおり。

import numpy as np
import scipy.stats as stats
chi2, p, df, expected = stats.chi2_contingency(observed)

 引数として与えるobserved(観測度数)は、numpy の array(2次元配列)です。

 戻り値は次のとおり。

 観測度数と期待度数のズレをみて、「2つの事象が独立である」という仮説を
棄却できるほどのズレなのか、それほどでもないのかを検証するのがカイ2乗検定。

 棄却できる(関連性がある)と判断される場合は残差分析を行います。

    

 observed, expected の2つを材料として
残差情報を次のように求めることができます。

res = observed - expected  # 残差
residuals = res / np.sqrt(expected)  # 標準化残差
tsum = observed.sum()*1.0  # 観測度数の計
relm = 1.0 - np.sum(observed, axis=1) / tsum  # 行要素
celm = 1.0 - np.sum(observed, axis=0) / tsum  # 列要素
mouter = np.outer(relm, celm)  # 行要素×列要素:行列の外積
stdres = residuals / np.sqrt(mouter)  # 調整済み残差

 residuals(標準化残差), stdres(調整済み残差)は
どちらも numpyのarray(2次元配列)です。

 カイ2乗検定で2つの事象が独立ではない(何らかの関連性がある)となった場合
最後に求めた調整済み残差が分析の重要な手がかりになります。

 なお、2つの事象の連関の度合いを示すクラメール係数は次のように算出。

cramer = np.sqrt(chi2 / (tsum*(min(observed.shape)-1)))

 クラメール係数は 0〜1 の値をとり、連関が強いほど大きな値になります。

目次に戻る


2. Prastクラスの chisq_test() によるカイ2乗検定

 Prastクラスに chisq_test() を設けました。

 カイ2乗検定を行い、残差関係の情報とクラメール係数も込みで返します。

Prast.chisq_test(gkey1, gkey2)
dod = psx.chisq_test("gender", "opinion")

 引数には2つの列ラベルを指定します(2つとも必須)。

 chisq_test("gender", "opinion") とすれば、
性別と意見のクロス集計表(合計欄がないもの)を内部で生成し、
それをカイ2乗検定にかけます。

    

 戻り値のdod(OrderedDict)のkey, valueは下のとおり。

 今回、p_valueが0.05より大きいので有意性が認められません。

 つまり、性別と意見の間に有意な関連性があるとはいえないと判断します。

 この場合、残差分析は行わず、調整済み残差は参照しません。

    

 戻り値に含まれる DataFrame の小数点以下の桁数ですが、
cround (Prastクラスのメンバー変数)の値で調整することはできません。

 桁数を変更したいときは、それぞれに pythonのround()を適用します。

dod = psx.chisq_test("gender", "opinion")
for key in ['expected', 'residuals', 'stdres']:
    dod[key] = dod[key].round(3)

 上のようにすると、3つの DataFrame の小数点以下の桁数が3桁以内になります。

目次に戻る


3. スクリプトの例

 カイ2乗検定のサンプルスクリプトを掲げます。

 地域×意見を検定にかけて、その結果を検証します。

  賛成 反対 保留
海岸 47 22 10
丘陵 38 32 9
湖畔 28 39 11
川沿い 12 60 8
都市部 28 41 10

 以下、スクリプトです。

 1# chisq01.py (coding: cp932)
 2import pandas as pd
 3from prast import Prast
 4
 5dtf = pd.read_csv("data01.csv")
 6psx = Prast(dtf, "data01_c.txt", "data01_i.txt", "cp932")
 7dod = psx.chisq_test("area", "opinion")
 8for key in ['expected', 'residuals', 'stdres']:
 9    dod[key] = dod[key].round(3)  # 小数点以下の桁数を調整
10for key in dod.keys():
11    print(key)
12    print(dod[key])
13    print('')

 上のスクリプトを実行すると、p_value が 9.85593038005e-07 と算出されます。

 これは 0.05 より小さいので、地域と意見の間に何らかの関連性があると見ます。

 そこで、調整済み残差を確認すると下のとおり。

  賛成 反対 保留
海岸 4.235 -4.227 0.154
丘陵 1.911 -1.711 -0.231
湖畔 -0.574 0.175 0.589
川沿い -4.880 5.186 -0.660
都市部 -0.671 0.554 0.154

 有意水準を5%とした場合、各セルの値 v が
v<-1.96 なら該当のセルが有意に小さい
v>1.96 なら有意に大きいということになります。

 有意水準を1%とした場合は 1.96 を 2.58 に置き換えます。

 上の調整済み残差をみると、
「海岸」では賛成が有意に多く、反対が有意に少ない。
「川沿い」では賛成が有意に少なく、反対が有意に多い。
ということが分かります。

 その他のセルでは有意性が認められません。

目次に戻る


4. コクランの規則とwarning

 カイ2乗検定を行う場合、期待度数の表をみて
5未満のセルの個数が全体の20%未満でないと
適切な検定結果が得られない、という考え方があります。

 これをコクランの規則というようです。これには異論もあるようですが……

 data01.csv では欠損値がごく少数なので
欠損値を組み入れたクロス集計表を chisq_test() にかけると
warningが標準出力に出力されます。

 性別×意見の場合、次のようなメッセージになります。

警告: '期待度数が5未満のセルの割合' >= 20% | 50.0%(6/12)

 セルの総数 12個のうち、6個が期待度数5未満であり、
その割合は 50%となり、20%以上になっているよ、という警告です。

 chisq_test() の戻り値 dod でも dod['warning'] が設けられ
その値として上記の警告メッセージを得ることができます。

 dod['warning'] がなければコクランの規則に抵触しないということになります。

目次に戻る


5. ごく少数の行または列を対象外にする

 コクランの規則に抵触しないようにするためには
セルの値がごく少数の行または列を対象外とすることになるでしょうか。

 今回「地域×意見」では欠損値を含めなければ抵触しませんが、
あえて「C:海岸」と「R:川沿い」を対象外にしてみます。

 方法は2つです。

    

(1) 別名割当てを変更する

 Prastクラスのメソッドで集計する場合、
別名が定義されているなら、定義にないものは対象外になります。

 別名割当てファイル(data01_i.txt)を書き換えてもいいのですが、
一時的に外すだけなら put_indexes() で定義し直す方がいいとおもいます。

 具体的には下のようなスクリプトです。

 1# chisq02.py (coding: cp932)
 2import pandas as pd
 3from prast import Prast
 4
 5dtf = pd.read_csv("data01.csv")
 6psx = Prast(dtf, "data01_c.txt", "data01_i.txt", "cp932")
 7iod_org = psx.get_indexes()  # 念のためオリジナルのindexesを保持
 8psx.put_indexes('''
 9area
10H,丘陵
11L,湖畔
12U,都市部
13''')
14
15dod = psx.chisq_test("area", "opinion")
16for key in dod.keys():
17    print(key)
18    print(dod[key])
19    print('')
20psx.put_indexes(iod_org)  # indexesを元に戻す

    

(2) 欠損値として設定する

 もう1つの方法として、対象外にしたいものを欠損値にする場合は
recona() を使います。

(前略)
psx = Prast(dtf, "data01_c.txt", "data01_i.txt", "cp932")
for x in ['C', 'R']:
    psx.recona("area", x)
dod = psx.chisq_test("area", "opinion")
(後略)

 この方法の場合、欠損値を元に戻すのが少しやっかいです。

 以前は C だったものと R だったものが一律に NaN になってしまうからです。

 元に戻したいときは、欠損値に置き換える前の area の列を記録しておいて
検定処理終了後にその列を元に戻すということになるでしょうか。

目次に戻る


6. クラスに属さない chisq_test()

 Prastクラスに属さない chisq_test() も定義してあります。

import prast as ps
dod = ps.chisq_test(dtf)

 上のように引数(DataFrame)を1つだけ指定します。

 既存のクロス集計表があって、それをカイ2乗検定にかけたいときは
こちらの chisq_test() の方を使います。

 引数のクロス集計表は、pandasのDataFrameを与えます。
numpyのarrayではありません。

 行ラベルも列ラベルもない、数字だけからなる行列を検定する例を掲げます。

 1# chisq03.py (coding: cp932)
 2import os
 3import pandas as pd
 4import prast as ps
 5
 6csv_data = '''\
 717,1,5
 84,6,14
 97,13,9
108,9,2
11'''
12tfile = "tmp.csv"  # 一時的に設けるファイルの名前
13with open(tfile, "w") as f:
14    f.write(csv_data)
15dtf = pd.read_csv(tfile, header=None)
16os.remove(tfile)  # 一時的ファイルを削除
17dod = ps.chisq_test(dtf)
18for key in dod.keys():
19    print(key)
20    print(dod[key])
21    print('')

 行列をcsvファイルとして書き出し、
それを read_csv() で読み込んで DataFrameを生成します。

 csvデータには列ラベルがない(ヘッダがない)ので
pd.read_csv(tfile, header=None) のようにheaderオプションを指定。

    

 行ラベルと列ラベルが付いている行列を検定するケースも載せておきます。

 read_csv() のオプションとして index_col=0 を指定します。

 1# chisq04.py (coding: cp932)
 2import os
 3import pandas as pd
 4import prast as ps
 5
 6csv_data = '''\
 7,A,B,C
 8W,17,1,5
 9X,4,6,14
10Y,7,13,9
11Z,8,9,2
12'''
13tfile = "tmp.csv"  # 一時的に設けるファイルの名前
14with open(tfile, "w") as f:
15    f.write(csv_data)
16dtf = pd.read_csv(tfile, index_col=0, encoding="cp932")
17os.remove(tfile)  # 一時的ファイルを削除
18dtf.index = list(dtf.index)
19dod = ps.chisq_test(dtf)
20for key in dod.keys():
21    print(key)
22    print(dod[key])
23    print('')

 行ラベルと列ラベルはアルファベットですが、半角ではなく全角文字です。

 そのため read_csv() で encoding オプションを指定しています。

行ラベルがあるcsvを読み込むときは index_colオプションを付けて
pd.read_csv(tfile, index_col=0, encoding="cp932") とします。

〜 以上 〜

Copyright (C) T. Yoshiizumi, 2018 All rights reserved.