カイ二乗検定でカテゴリデータを分析する(scipy.stats.chi2_contingency)

カイ二乗検定でカテゴリデータを分析する(scipy.stats.chi2_contingency)

前回の記事では、t検定を使って「2つのグループの平均値に差があるか」を判定する方法を学びました。

t検定をPythonで実行してみよう(scipy.stats.ttest)

しかし、データ分析の現場では、数値ではなくカテゴリ(分類)で分けられたデータを扱うことも多くあります。

たとえば……

  • 性別と購買行動に関連はあるか?
  • 広告Aと広告Bでクリック率に違いはあるか?

……といった問です。

こうしたカテゴリデータ同士の関連性を調べるのが、カイ二乗検定(カイにじょうけんてい) です。

今回は、カイ二乗検定の基本的な考え方と、SciPyを使ってPythonで実行する方法を、できるだけ簡単に解説します。

カテゴリデータとは?

まず、「カテゴリデータ」について確認しておきましょう。

カテゴリデータ(質的データ) とは、数値ではなく「分類」や「種類」で表されるデータのことです。

  • 性別:男性 / 女性
  • 血液型:A型 / B型 / O型 / AB型
  • 購入の有無:買った / 買わなかった
  • 満足度:満足 / 普通 / 不満
  • 広告の種類:広告A / 広告B / 広告C

これに対して、テストの点数(78点)や身長(170cm)のように数値で測れるデータは「数値データ(量的データ)」と呼ばれます。

前回紹介したt検定は数値データ向けの検定でした。

今回紹介するカイ二乗検定は、カテゴリデータ向けの検定です。

 

カイ二乗検定とは?

カイ二乗検定(χ²検定) とは、2つのカテゴリ変数の間に関連性があるかどうかを判定する方法です。

たとえば、以下のような疑問に答えることができます。

  • 性別」と「商品を買うかどうか」に関連はあるか?
  • 年代」と「好きなSNS」に関連はあるか?
  • 広告の種類」と「クリックするかどうか」に関連はあるか?

カイ二乗検定の考え方はシンプルです。

  • 【1】もし2つのカテゴリに「関連がない」と仮定したら、データはこうなるはず(期待度数)
  • 【2】実際のデータ(観測度数)と比べてみる
  • 【3】ズレの大きさを評価
    • ズレが大きい → 「関連がない」とは考えにくい → 関連がある!
    • ズレが小さい → 偶然の範囲内 → 関連があるとは言えない

この「ズレの大きさ」を数値化したものがカイ二乗統計量(χ²値)で、そこから計算される確率がp値です。

 

分割表(クロス集計表)とは?

カイ二乗検定を行うには、まずデータを分割表(ぶんかつひょう)の形に整理する必要があります。

分割表(クロス集計表) とは、2つのカテゴリの組み合わせごとに、データの件数を表にまとめたものです。

たとえば、100人のお客さんに「性別」と「商品Xを購入したかどうか」を調べた結果を、分割表にすると以下のようになります。

  購入した 購入しなかった 合計
男性 30 20 50
女性 15 35 50
合計 45 55 100

この表を見ると、男性のほうが購入率が高そうに見えます。

しかし、この差は偶然かもしれません

カイ二乗検定を使えば、この差が統計的に意味のあるものかどうかを判定できます。

補足:度数(どすう)とは?

度数とは、各カテゴリに該当するデータの個数(件数)のことです。上の表では、「男性かつ購入した」の度数は30です。分割表に記載された実際のデータの度数を観測度数(かんそくどすう)、関連がないと仮定した場合に期待される度数を期待度数(きたいどすう)と呼びます。

 

期待度数の考え方

カイ二乗検定のカギとなる「期待度数」について、もう少し詳しく見てみましょう。

もし「性別」と「購入の有無」にまったく関連がなければ、男性でも女性でも購入率は同じになるはずです。

全体の購入率は 45人 ÷ 100人 = 45% です。

もし関連がなければ、以下のような度数になります。これが期待度数です。

  • 男性50人のうち 45% が購入 → 50 × 0.45 = 22.5人
  • 男性50人のうち 55% が未購入 → 50 × 0.55 = 27.5人
  • 女性50人のうち 45% が購入 → 50 × 0.45 = 22.5人
  • 女性50人のうち 55% が未購入 → 50 × 0.55 = 27.5人

期待度数の計算式は以下のとおりです。

期待度数 = (その行の合計 × その列の合計) ÷ 全体の合計

 

期待度数を表にすると次のようになります。

  購入した 購入しなかった 合計
男性 22.5 27.5 50
女性 22.5 27.5 50
合計 45.0 55.0 100

観測度数(実際のデータ)期待度数(関連がない場合の理論値) を比較します。

セル(属性×結果) 観測度数 期待度数 ズレ
男性×購入 30 22.5 +7.5(多い)
男性×未購入 20 27.5 -7.5(少ない)
女性×購入 15 22.5 -7.5(少ない)
女性×未購入 35 27.5 +7.5(多い)

すべてのセルで大きなズレがあります。このズレが「偶然の範囲内」なのか「統計的に意味があるレベル」なのかを、カイ二乗検定で判定します。

 

SciPyでカイ二乗検定を実行しよう

 実践①:性別と購買行動の関連性を調べる

先ほどの「性別×購入の有無」のデータを、SciPyで検定してみましょう。

以下、コードです。

import numpy as np
from scipy import stats

# 分割表を作成(2次元配列)
# 行:男性, 女性
# 列:購入した, 購入しなかった
observed = np.array([
    [30, 20],   # 男性:購入30人, 未購入20人
    [15, 35]    # 女性:購入15人, 未購入35人
])

# カイ二乗検定を実行
chi2, p_value, dof, expected = stats.chi2_contingency(observed)

print(f"カイ二乗統計量 : {chi2:.4f}")
print(f"p値            : {p_value:.4f}")
print(f"自由度         : {dof}")
print(f"期待度数       :")
print(expected)
  • import numpy as np:NumPyライブラリを np という短い名前で読み込みます。配列(表形式のデータ)を作るために使います
  • observed = np.array([[30, 20], [15, 35]]):分割表のデータを 2次元配列(表のような形のデータ)として作成します。1行目が男性、2行目が女性のデータです
  • chi2, p_value, dof, expected = stats.chi2_contingency(observed):カイ二乗検定を実行し、4つの結果を受け取ります

chi2_contingency() が返す4つの値の意味は以下のとおりです。

変数名 意味
chi2 カイ二乗統計量(ズレの大きさを表す数値)
p_value p値(この結果が偶然に起こる確率)
dof 自由度(データの自由に動ける次元数)
expected 期待度数(関連がないと仮定した場合の理論値)

 

以下、実行結果です。

カイ二乗統計量 : 7.9192
p値            : 0.0049
自由度         : 1
期待度数       :
[[22.5 27.5]
 [22.5 27.5]]

 

p値は0.0049で、有意水準0.05を大きく下回っています。つまり、性別と購買行動の間には統計的に意味のある関連があると判断できます。

期待度数を見ると [[22.5, 27.5], [22.5, 27.5]] で、先ほど手計算した値と一致していることも確認できます。

補足:自由度(じゆうど)とは?

自由度(degrees of freedom、略してdof)とは、データが自由に取りうる値の数のことです。分割表の場合、自由度は (行数 - 1) × (列数 - 1) で計算します。今回は (2-1) × (2-1) = 1 です。自由度はカイ二乗統計量からp値を計算するために内部的に使われますが、chi2_contingency() が自動で処理してくれるので、初心者の方は「こういう値もあるんだな」程度の理解で問題ありません。

 

 実践②:年代とSNS利用の関連を調べる

今度は、カテゴリが3つ以上ある場合の例を見てみましょう。

「年代(20代・30代・40代)」と「最もよく使うSNS(X・Instagram・Facebook)」に関連があるかを調べます。

以下、コードです。

import numpy as np
from scipy import stats

# 分割表を作成(3×3の表)
# 行:20代, 30代, 40代
# 列:X(Twitter), Instagram, Facebook
observed = np.array([
    [40, 35, 5],    # 20代
    [25, 30, 25],   # 30代
    [10, 15, 55]    # 40代
])

# カイ二乗検定を実行
chi2, p_value, dof, expected = stats.chi2_contingency(observed)

print(f"カイ二乗統計量 : {chi2:.4f}")
print(f"p値            : {p_value:.6f}")
print(f"自由度         : {dof}")
print()
print("--- 観測度数(実際のデータ)---")
print(observed)
print()
print("--- 期待度数(関連がない場合の理論値)---")
print(np.round(expected, 1))  # 小数第1位で四捨五入して表示
  • np.array([[40, 35, 5], [25, 30, 25], [10, 15, 55]]):3行×3列の分割表を作成しています。2×2だけでなく、より大きな表にも対応できます
  • np.round(expected, 1):期待度数を小数第1位で四捨五入して見やすくしています。np.round() はNumPyの四捨五入関数です

 

以下、実行結果です。

カイ二乗統計量 : 70.8309
p値            : 0.000000
自由度         : 4

--- 観測度数(実際のデータ)---
[[40 35  5]
 [25 30 25]
 [10 15 55]]

--- 期待度数(関連がない場合の理論値)---
[[25.  26.7 28.3]
 [25.  26.7 28.3]
 [25.  26.7 28.3]]

 

期待度数(関連がない場合)と比べて、観測度数に大きな偏りが見られます。

たとえば、20代はXとInstagramの利用が期待値より多く、40代はFacebookが期待値より大幅に多いです。年代によって利用するSNSの傾向が異なることが統計的に確認できました。

 

 実践③:関連が見られないケース

「関連があるとは言えない」ケースも確認しておきましょう。

以下、コードです。

import numpy as np
from scipy import stats

# 分割表:朝食の種類(パン/ごはん)× テスト合否(合格/不合格)
observed = np.array([
    [48, 52],   # パン派:合格48人, 不合格52人
    [50, 50]    # ごはん派:合格50人, 不合格50人
])

# カイ二乗検定を実行
chi2, p_value, dof, expected = stats.chi2_contingency(observed)

print(f"カイ二乗統計量: {chi2:.4f}")
print(f"p値: {p_value:.4f}")
print()

# 判定
alpha = 0.05
if p_value < alpha:
    print(f"p値({p_value:.4f}) < 有意水準({alpha})")
    print("→ 有意な関連がある")
else:
    print(f"p値({p_value:.4f}) ≥ 有意水準({alpha})")
    print("→ 有意な関連があるとは言えない")

 

以下、実行結果です。

カイ二乗統計量: 0.0200
p値: 0.8875

p値(0.8875) ≥ 有意水準(0.05)
→ 有意な関連があるとは言えない

 

p値は0.7751で、有意水準0.05を大きく上回っています。朝食の種類とテスト合否の間には、統計的に意味のある関連は見られません。

 

t検定とカイ二乗検定の使い分け

前回紹介したt検定と今回のカイ二乗検定は、データの種類によって使い分けます。

比較項目 t検定 カイ二乗検定
扱うデータ 数値データ カテゴリデータ
調べること 2グループの平均値に差があるか 2つのカテゴリに関連があるか
入力の形 2つの数値リスト 分割表(クロス集計表)
SciPyの関数 ttest_ind() / ttest_rel() chi2_contingency()
使用例 AクラスとBクラスの成績比較 性別と購買行動の関連分析

 

カイ二乗検定を使うときの注意点

 ① 期待度数が小さすぎないこと

カイ二乗検定は、すべてのセルの期待度数が5以上であることが望ましいとされています。

期待度数が5未満のセルが多い場合は、カイ二乗検定の信頼性が低下します。

その場合は、フィッシャーの正確確率検定(Fisher’s exact test) を使うことが推奨されます。

以下、コードです。

# フィッシャーの正確確率検定(2×2の分割表のみ対応)
odds_ratio, p_value = stats.fisher_exact(observed)

print(f"オッズ比: {odds_ratio:.4f}")
print(f"p値: {p_value:.4f}")

 

以下、実行結果です。

オッズ比: 0.9231
p値: 0.8876

 

 ② 度数(件数)を入れること

分割表に入れるのは 度数(件数) です。割合(%)や平均値を入れるのは誤りです。

  • 正しい入力:[[30, 20], [15, 35]] ← 人数(件数)
  • 誤った入力:[[0.6, 0.4], [0.3, 0.7]] ← 割合

 

 ③ 各データは独立であること

1人の回答者が複数のカテゴリに重複してカウントされていないことが前提です。

たとえば、同じ人が複数回答に含まれていると、結果が正しくなりません。

 

まとめ

今回の内容のポイントを振り返ります。

  • カイ二乗検定 は、2つのカテゴリ変数の間に関連性があるかを判定する方法
  • 分割表(クロス集計表) にデータを整理してから検定を行う
  • 観測度数と期待度数のズレが大きいほど、関連がある可能性が高い
  • SciPyでは stats.chi2_contingency() で簡単に実行できる
  • p値 < 0.05 なら「統計的に有意な関連がある」と判定
  • 数値データにはt検定、カテゴリデータにはカイ二乗検定と使い分ける

次回は、「scipy.optimizeで最適化問題を解く入門」 をテーマに、コスト関数の最小化やパラメータチューニングの基本を紹介します。