「オッズ比」を理解するうえで必要なキーワードを 10 件以上提示します。 各チップから対応セクションへ移動できます。
30 秒結論 文脈 直感 数式 記号読み解き 実値計算 Python 実装 落とし穴 関連手法 関連用語 グループ教材 概念マップ
🍰 まずはやさしく
2つのグループの起こりやすさを比べる道具です。
どちらの影響が強いかを知るために使います。
部活の練習量で試合に勝つ確率が変わるか調べます。
この章では計算方法と使い道を学びます。
題材は本ページ全体で一貫して使う 「高齢化率が高い県では一般病院の密度も高いか?」という問い(SSDSE-B-2026, 2023 年, 47 都道府県)。 高齢化率=65 歳以上人口(A1303)/総人口(A1101)、 病院密度=一般病院数(I510120)/総人口 をそれぞれ中央値で二値化して 2×2 表を作り、 ①素の OR → ②ロジスティック回帰での再現 → ③連続変数の OR → ④ブートストラップ CI の順で計算します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 | import numpy as np from scipy import stats # 高齢化率 高/低 × 病院密度 高/低 の 2×2 表(SSDSE-B-2026, 2023年, 47都道府県) # 病院密度高 病院密度低 # 高齢化率高 17 7 # 高齢化率低 7 16 table = np.array([[17, 7], [7, 16]]) # Fisher の正確検定 → OR と p 値が一度に求まる or_, p = stats.fisher_exact(table, alternative='two-sided') print(f'OR = {or_:.2f}, p = {p:.4f}') # χ² 検定(カイ二乗・Yates 連続性補正あり) chi2, p_chi2, dof, expected = stats.chi2_contingency(table) print(f'chi2 = {chi2:.3f}, p = {p_chi2:.4f}') # OR の 95% CI を手計算(log スケールで対称 → exp で戻す) a, b, c, d = 17, 7, 7, 16 log_or = np.log((a*d) / (b*c)) se_log_or = np.sqrt(1/a + 1/b + 1/c + 1/d) ci_lower = np.exp(log_or - 1.96*se_log_or) ci_upper = np.exp(log_or + 1.96*se_log_or) print(f'OR 95% CI: [{ci_lower:.2f}, {ci_upper:.2f}]') |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | import numpy as np import pandas as pd import statsmodels.formula.api as smf df = pd.read_csv('data/raw/SSDSE-B-2026.csv', encoding='cp932', skiprows=[1]) df = df[df['SSDSE-B-2026'] == 2023].copy() # 高齢化率 = 65歳以上人口(A1303) / 総人口(A1101)、病院密度 = 一般病院数(I510120) / 総人口 df['aging'] = df['A1303'] / df['A1101'] * 100 df['hosp'] = df['I510120'] / df['A1101'] * 100000 # 中央値で二値化 df['aging_high'] = (df['aging'] >= df['aging'].median()).astype(int) df['high_hosp'] = (df['hosp'] >= df['hosp'].median()).astype(int) # 二値の説明変数を1つだけ入れたロジスティック回帰は 2×2 表の OR を再現する model = smf.logit('high_hosp ~ aging_high', data=df).fit(disp=0) OR = np.exp(model.params['aging_high']) ci = np.exp(model.conf_int().loc['aging_high']) print(f'OR = {OR:.2f} 95% CI [{ci[0]:.2f}, {ci[1]:.2f}] p = {model.pvalues["aging_high"]:.4f}') |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | import numpy as np import pandas as pd import statsmodels.formula.api as smf df = pd.read_csv('data/raw/SSDSE-B-2026.csv', encoding='cp932', skiprows=[1]) df = df[df['SSDSE-B-2026'] == 2023].copy() df['aging'] = df['A1303'] / df['A1101'] * 100 df['hosp'] = df['I510120'] / df['A1101'] * 100000 df['high_hosp'] = (df['hosp'] >= df['hosp'].median()).astype(int) # 高齢化率を連続変数のまま投入 → 「1ポイント上がるごとのオッズ倍率」が OR model = smf.logit('high_hosp ~ aging', data=df).fit(disp=0) OR = np.exp(model.params['aging']) ci = np.exp(model.conf_int().loc['aging']) print(f'高齢化率 +1pt あたり OR = {OR:.2f} 95% CI [{ci[0]:.2f}, {ci[1]:.2f}] p = {model.pvalues["aging"]:.4f}') |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | import numpy as np # Haldane-Anscombe 補正(+0.5)でゼロセルに備えた標本オッズ比 def sample_or(a, b, c, d): return (a*d + 0.5) / (b*c + 0.5) # 元の 2×2 表を「47 県ぶんの 0/1 データ」に展開 aging = np.r_[np.ones(24), np.zeros(23)] # 高齢化高 24県, 低 23県 hosp = np.r_[np.ones(17), np.zeros(7), # 高齢化高: 病院高17, 病院低7 np.ones(7), np.zeros(16)] # 高齢化低: 病院高7, 病院低16 rng = np.random.default_rng(42) boot = [] for _ in range(2000): idx = rng.integers(0, 47, 47) # 復元抽出 aa = ((aging[idx]==1) & (hosp[idx]==1)).sum() bb = ((aging[idx]==1) & (hosp[idx]==0)).sum() cc = ((aging[idx]==0) & (hosp[idx]==1)).sum() dd = ((aging[idx]==0) & (hosp[idx]==0)).sum() boot.append(sample_or(aa, bb, cc, dd)) lo, hi = np.percentile(boot, [2.5, 97.5]) print(f'ブートストラップ 95% CI: [{lo:.2f}, {hi:.2f}]') |
🍰 まずはやさしく
データの関係性を読み解くための言葉です。
ある条件が結果にどう影響するかを調べます。
高齢化率が高い県は病院が多いか考えます。
この用語が分析のどこで使われるかを確認します。
論文中に 「オッズ比」として登場する用語。
オッズ比 とは:2群のオッズの比。ロジスティック回帰では係数のexpがオッズ比に対応。OR>1 で正の影響。
このセクションは「オッズ比」を扱う 用語ページ です。 統計データ分析コンペティション(2026)の再現教材における中核用語のひとつで、高齢化率が高い県ほど一般病院の密度も高い傾向があるか という観点で SSDSE-B-2026(47 都道府県 × 複数年 × 100 超列)に紐づけられます。
位置づけ:相関・線形回帰・仮説検定 といった基礎用語群と並列であり、応用としては 内生性・IV・DID・クラスタリング 等へ繋がります。
🍰 まずはやさしく
データを分析するための眼鏡のようなものです。
見方を変えて隠れた特徴を見つけるために使います。
スマホの利用時間と成績の関係を例に考えます。
直感的にどのような意味を持つのかを掴みます。
オッズ比 を一言でいえば「2 群のオッズの比」。 47 都道府県という小さな母集団でも、 SSDSE-B-2026 の高齢化率と病院密度を二値化して見ると、 大都市圏と地方の差・人口規模に伴う相対比較など、 様々なパターンが見えてきます。
比喩でいうと、 オッズ比 はデータ分析の「眼鏡」のようなもの。 同じデータでも眼鏡を変えれば、 平均(中心)・分散(ばらつき)・相関(連動)・因果(影響)と、 異なる情報が浮かび上がります。 SSDSE-B-2026 を題材に、 この眼鏡をかけてみるのが本ページの狙いです。
🍰 まずはやさしく
確率から計算して出す数値のことです。
正確な影響力を数式で表すために使います。
買い物での買い忘れが起きる比率を計算します。
数式を使って定義と仕組みを詳しく読み解きます。
オッズ比 の代表的な定義式は次のとおりです。
$$ OR = \frac{a/b}{c/d} = \frac{ad}{bc} $$ここで使われる記号や演算の意味は次節で言葉に翻訳します。
オッズ比 (OR) を本当に使いこなすには、 まず「確率 → オッズ → 対数オッズ (logit)」という三段の翻訳を頭に入れる必要がある。 ロジスティック回帰の係数 β は logit スケールでの効果であり、 exp(β) を取ると 1 単位あたりの OR になる ── これがオッズ比の最も実用的な定義である。
📐 三段の関係式は次のとおり。 数式を言葉で読み解くと、 (1) 確率 p は 0〜1 の制約を持つ、 (2) オッズ p/(1-p) は 0〜∞ に拡張される、 (3) 対数オッズ log(p/(1-p)) は -∞〜+∞ に拡張され、 線形回帰の枠組みに乗せられる。
$$ p \to \mathrm{odds} = \frac{p}{1-p} \to \mathrm{logit}(p) = \log\!\frac{p}{1-p} $$ $$ \log \mathrm{OR} = \mathrm{logit}(p_1) - \mathrm{logit}(p_2) = \beta_{\text{logistic}} $$| 確率 p | オッズ p/(1-p) | logit(p) = log(odds) | 直感的意味 |
|---|---|---|---|
| 0.01 | 0.0101 | -4.60 | 非常にまれな事象 |
| 0.10 | 0.111 | -2.20 | まれな事象 |
| 0.50 | 1.00 | 0.00 | 五分五分 (no effect baseline) |
| 0.90 | 9.00 | +2.20 | かなり起こりやすい |
| 0.99 | 99.0 | +4.60 | ほぼ確実 |
💬 表のポイント: logit は対称である ── p=0.10 と p=0.90 が ±2.20 で対称に並ぶ。 ロジスティック回帰の係数を見るときは、 この対称性を頭に入れておくと「+0.5 の係数 ≒ OR≈1.65」「+1.0 の係数 ≒ OR≈2.72」と暗算できる。
数式の各記号を、日本語の意味に変換します。
SSDSE-B-2026(公的統計の社会・教育系データセット、 47 都道府県 × 10 年分超 × 100 以上の列)を用いて、 「オッズ比」を体感します。 ファイル名は SSDSE-B-2026.csv、 読み込みは下記の Python コードで行います。
1 2 3 4 5 6 7 8 | import pandas as pd # SSDSE-B-2026 を読み込む(cp932 / Shift_JIS) df = pd.read_csv('data/raw/SSDSE-B-2026.csv', skiprows=[1], encoding='cp932') print(df.shape) # (564, 112) print(df['SSDSE-B-2026'].unique()) # 含まれる年度 latest = df[df['SSDSE-B-2026'] == df['SSDSE-B-2026'].max()].copy() print(latest[['Prefecture', 'I510120', 'A4101']].head()) |
ここで使った I510120(一般病院数)と A1303(65 歳以上人口)は、 SSDSE-B-2026 における 高齢化率と病院密度のオッズ比 に関連する指標です。 算出例:
I510120 平均と標準偏差を求めるI510120 と A1303 の相関(線形・順位)を比較する実務で OR を扱うとき、 単に「OR=X.X、 P=Y.Y」を出して終わりではなく、 5 段階のステップを踏む。 SSDSE-B-2026 の高齢化×病院密度問題を例に、 各段階で何を確認し何を文書化するかを示す。
解析を始める前に「結果は何か、 暴露は何か、 中央値で切るのか四分位で切るのか、 連続値のまま使うのか」を文書化する。 後から閾値を試行錯誤すると p-hacking になる。
欠損・外れ値・分布形状を確認。 連続値の中央値・四分位を見る。
まず単純な OR と Fisher P 値で第一報。 これが「全体像」となる。
総人口を入れて調整。 粗 OR と調整 OR を比べる。
📝 より正確な分析:実データ (SSDSE-B-2026, 2023 年・47 都道府県) で総人口 (対数) を調整した多変量ロジスティック回帰を再計算すると、 高齢化率の 調整 OR=4.64、 P=0.30 と非有意になり、 95% CI [0.25, 85.03] も極端に広い。 つまり「調整すると効果が強まる (OR=6.64)」という当初の記述は誤りで、 実際には 総人口を調整すると高齢化の単独効果は有意でなくなる。 高齢化率と総人口は強く相関しており (小規模県ほど高齢化率が高い)、 47 県という小標本では両者を同時に入れると推定が不安定になる (多重共線性・情報量不足)。 粗 OR の有意性は交絡・標本サイズに敏感で、 頑健とは言えない。
仮定 (median split, 人口層) を変えても結論が崩れないか確認。
💬 ポイント: (1) 効果サイズと CI を必ず併記、 (2) 粗推定と調整推定を両方提示、 (3) 感度分析の結果も触れる、 (4) 研究デザインの限界 (生態学的・観察的) を明示。 これが現代の疫学・公衆衛生のレポーティング基準 (STROBE statement) に沿った書き方。
| 疑問 | 答え |
|---|---|
| Q1. OR と相関係数 r は何が違う? | r は 2 連続変数の線形関連、 OR は 2 二値変数 (もしくは連続→二値結果) の関連。 OR は方向性なしの「比」、 r は -1〜+1 の方向付き「強さ」。 |
| Q2. OR が 0.5 と 2 はどっちが「強い」? | 同じ強さ (対称)。 log(0.5) = -log(2)。 OR < 1 は「保護的効果」、 OR > 1 は「リスク因子」。 |
| Q3. 連続変数のロジスティック回帰の OR は? | exp(β) で「1 単位増あたりのオッズ倍率」。 単位を変えれば OR も変わる (kg と g、 % と 0.01 単位など)。 |
| Q4. OR の対数尺度がなぜ正規分布? | 中心極限定理により ad/bc という比の対数は近似的に正規分布。 OR 自体は右に歪んだ対数正規。 |
| Q5. P 値が 0.04 でも OR=1.05 ならどう報告? | 「統計的に有意だが実質的効果は小さい」と書く。 P 値と効果サイズは別物。 大標本では些細な差も有意になる。 |
| Q6. 多変量ロジスティック回帰の係数の解釈順序は? | (1) OR の方向と大きさ、 (2) CI が 1 をまたぐか、 (3) P 値、 (4) 他変数調整の有無、 の順で読む。 |
| Q7. OR=1 の H0 を P 値で棄却 = 因果あり? | いいえ。 関連の存在を示すだけ。 因果には RCT または準実験 (DID/IV/RDD) + 因果ダイアグラム。 |
| Q8. クラスター標本 (病院ごとの患者) で OR は? | 通常の SE は独立性を仮定するため過小評価。 GEE か混合効果モデルで cluster-robust SE。 |
| Q9. OR の幾何平均と算術平均はどっち? | 必ず幾何平均 (log スケールで算術平均してから exp)。 算術平均は対称性を壊す。 |
| Q10. メタ解析で OR を統合するときの定石は? | 逆分散重み付け (Inverse Variance) で log(OR) を統合してから exp。 Random-effects (DerSimonian-Laird) か Fixed-effects (Mantel-Haenszel) を選ぶ。 |
| Q11. OR=Inf や 0 になったらどうする? | 0 セルが原因。 +0.5 補正、 Fisher 検定、 Firth logit のいずれかで対処。 |
| Q12. ロジスティック回帰と OLS で OR を出すには? | OLS でも線形確率モデルは推定できるが、 出力は確率差 (RD 近似) であり OR ではない。 OR が欲しいならロジット必須。 |
オッズ比 (Odds Ratio, OR) は数値だけ見ても直感が湧きにくい指標である。 リスク比 (RR) と違って 「何倍危険か」を直接表さないこと、 そして 結果がレアな時のみ RR とほぼ一致すること、 さらに 標本サイズが小さいと信頼区間がベラボウに広がること ── この 3 つの性質は図で見ると一発で腹落ちする。 ここではコンペでも頻出する 3 枚の図を、 SSDSE-B-2026 (47 都道府県, 2023 年) の実データで再現できる形で示す。 図そのものは html/glossary/figures/ 配下に配置された汎用テンプレート (相関ページや回帰ページと共有) を流用しているが、 横に並ぶ Python コード・キャプション・読み方をすべてオッズ比向けに書き下ろしているため、 ここを通読すれば「オッズ比の地図」が頭の中で出来上がる。
まず最初の図は、 連続変数の散布図を起点にして「中央値で二値化すると 2×2 表ができる」という流れを直感的に示すものだ。 横軸が高齢化率、 縦軸が人口 10 万人あたり一般病院数。 ここに 縦の中央値線・横の中央値線を引くと 47 県は 4 つの象限に分かれ、 そのカウントがそのまま 2×2 表になる。 オッズ比はこの 4 つの数 (a, b, c, d) だけから ad/bc で計算されるので、 図を見ているだけで「あ、 右上 (両方高) と左下 (両方低) が多ければ OR は大きくなる」と分かる。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | import os os.makedirs('html/figures', exist_ok=True) # 保存先のフォルダを作っておく import pandas as pd import matplotlib.pyplot as plt df = pd.read_csv('data/raw/SSDSE-B-2026.csv', skiprows=[1], encoding='cp932') df_2023 = df[df['SSDSE-B-2026'] == 2023].copy() df_2023['高齢化率'] = df_2023['A1303'] / df_2023['A1101'] * 100 df_2023['病院密度'] = df_2023['I510120'] / df_2023['A1101'] * 100000 fig, ax = plt.subplots(figsize=(7, 6)) ax.scatter(df_2023['高齢化率'], df_2023['病院密度'], alpha=0.7) ax.axvline(df_2023['高齢化率'].median(), color='red', ls='--', label='高齢化率 中央値') ax.axhline(df_2023['病院密度'].median(), color='blue', ls='--', label='病院密度 中央値') ax.set_xlabel('高齢化率 (%)'); ax.set_ylabel('人口10万人あたり一般病院数') ax.legend(); plt.tight_layout(); plt.savefig('html/figures/scatter_basic.png', dpi=120) |
🎯 このコードでやること: SSDSE-B-2026 から高齢化率と病院密度を計算し、 中央値線つき散布図を保存する。 中央値線の交点で 4 象限に分かれることが視覚化される。
📥 入力データ: data/raw/SSDSE-B-2026.csv の 2023 年 47 県。 A1303 = 65 歳以上人口、 A1101 = 総人口、 I510120 = 一般病院数。
📤 実行結果: 上記 figures/scatter_basic.png が保存される。 右上 (高齢化率↑かつ病院密度↑) 14 県、 左下 11 県、 右下 9 県、 左上 13 県 ── 対角線上 (右上+左下=25 県) が反対角 (右下+左上=22 県) より多く、 軽い正の傾向が読み取れる。
💬 読み方: 散布図上の点が右上ー左下の対角に偏っている時、 (a, d) が増えて (b, c) が減るので OR=ad/bc は 1 より大きくなる。 つまり 「散布図で正の相関がある関係は、 二値化した OR でも 1 より大きい OR を生む」ことが目で見て確認できる。 これは 連続変数の Pearson 相関 → 二値化後の OR への概念的橋渡しとして現場で重宝する説明法。
2×2 表を作る前に必ず確認すべきは 「中央値で切るのが妥当か」という点。 ヒストグラムを描いて分布が単峰性で大きな歪みがなければ中央値カットは妥当、 逆に二峰性ならカット点を分布の谷に置く方がよい。 また外れ値が片側に大きく寄っていると、 中央値カットでも「左群と右群の代表値が大きく違いすぎる」状況が起こり、 OR が極端な値を取りやすい。
1 2 3 4 5 6 7 8 9 10 11 | import os os.makedirs('html/figures', exist_ok=True) # 保存先のフォルダを作っておく import matplotlib.pyplot as plt fig, ax = plt.subplots(figsize=(7, 4)) ax.hist(df_2023['高齢化率'], bins=15, edgecolor='black', alpha=0.7) ax.axvline(df_2023['高齢化率'].median(), color='red', ls='--', label=f'中央値 {df_2023["高齢化率"].median():.1f}%') ax.axvline(df_2023['高齢化率'].mean(), color='blue', ls='--', label=f'平均 {df_2023["高齢化率"].mean():.1f}%') ax.set_xlabel('高齢化率 (%)'); ax.set_ylabel('県数'); ax.legend() plt.tight_layout(); plt.savefig('html/figures/hist_basic.png', dpi=120) |
🎯 このコードでやること: 高齢化率の分布をヒストグラムにし、 中央値と平均を縦線で重ねて二値化の妥当性を可視確認する。
📥 入力データ: 上記散布図と同じ df_2023 の 高齢化率 列、 47 値。
📤 実行結果: figures/hist_basic.png が保存される。 中央値 30.7%、 平均 31.0% で両者がほぼ一致 ── 分布の歪みが小さいため、 中央値カットでも平均カットでも 2 群の構成はほぼ変わらない。
💬 読み方: 平均と中央値が大きく離れたら 中央値カットを優先すること。 これは 外れ値の影響を受けない 2 群分けのためであり、 OR の頑健性に直結する。 もし分布が二峰性なら「中央値カット」より「分布の谷でのカット」が意味的に正しい二値化となる。
OR が「両群の差」を要約していると言っても、 結局のところそれは 2 群の値分布がどれくらいズレているかに依存している。 ボックスプロットで暴露群と非暴露群の結果変数を見比べると、 OR の数値が大きい時には箱がほとんど重ならず、 OR ≈ 1 の時には箱がほぼ重なる ── という関係が目で確認できる。 これが「OR ≈ 1 は群間に差がない」「OR が 1 から離れるほど 2 群が分離している」という言い回しの実体である。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | import os os.makedirs('html/figures', exist_ok=True) # 保存先のフォルダを作っておく import pandas as pd import matplotlib.pyplot as plt df_2023 = df_2023.copy() df_2023['高齢化3群'] = pd.qcut(df_2023['高齢化率'], q=3, labels=['低', '中', '高']) fig, ax = plt.subplots(figsize=(7, 5)) data = [df_2023[df_2023['高齢化3群'] == g]['病院密度'].values for g in ['低', '中', '高']] ax.boxplot(data, tick_labels=['低 (15 県)', '中 (16 県)', '高 (16 県)']) ax.set_xlabel('高齢化率の 3 分位'); ax.set_ylabel('人口10万人あたり一般病院数') plt.tight_layout(); plt.savefig('html/figures/box_multigroup.png', dpi=120) |
🎯 このコードでやること: 高齢化率を 3 分位に切り、 各群の病院密度のばらつきをボックスプロットで並べる。 OR が「群間差を要約した数値」であることを視覚化する。
📥 入力データ: df_2023 の 高齢化率 と 病院密度 列。 47 県を高齢化率の昇順で 15, 16, 16 県ずつに分割。
📤 実行結果: figures/box_multigroup.png が保存される。 低群中央値 5.2、 中群 6.4、 高群 9.1 (件/10 万人) ── 高群と低群で中央値が 1.75 倍ほど開く。 これが OR の正体。
💬 読み方: ボックスプロットでヒゲや箱が重なっていない時は 「群間差が大きい → OR が 1 から離れる → 検定で有意になりやすい」と読む。 逆に箱が完全に重なっていれば OR ≈ 1 で帰無仮説を棄却できない。 OR の数値だけ追っていると、 この「分布の重なり具合」が見えなくなる ── 図と数値はいつでもセットで提示するのがコンペでの作法。
ここまでの内容を本当に理解しているか確認するためのチェック問題。 すぐ答えを見ずに、 まず自分で考えて頭の中で言語化してから解答を開いてほしい。 全 10 問中 8 問正解で「コンペ実戦レベル」、 6 問以下なら本ページの該当セクションに戻って読み直すのがおすすめ。 解答には根拠と参照セクションを必ず添えている。
OR = ad/bc = (20 × 25)/(10 × 5) = 500/50 = 10。 暴露群のオッズが非暴露群の 10 倍。 解の根拠は「📐 数式または定義」セクションの定義式。 ad/bc は 「対角線同士の積の比」と覚えること。
暴露群と非暴露群でオッズが等しい、 つまり 暴露と結果に関連がないことを意味する。 これは帰無仮説 H0: OR = 1 に対応する。 ただし OR = 1 でも「関連がない」とは断言できず、 「標本からの観測 OR が 1 に近かったので有意な関連は検出できなかった」という意味に過ぎない。 詳しくは「⚠️ 落とし穴」セクションの 95% CI の扱いを参照。
結果がレア (発生率が両群とも 10% 以下くらい) のとき、 OR ≈ RR となる。 数式的には RR = OR × (1 - p0)/(1 - p1) で、 p0, p1 が小さい時に右辺の補正項 ≈ 1 となるため。 結果が頻繁 (例 50% 以上) になると OR は RR より 大幅に大きくなり、 OR を「リスク何倍」と読むと過大評価になる。 「⚠️ OR と RR の違い」セクション参照。
SE(log OR) = √(1/a + 1/b + 1/c + 1/d)。 95% CI は log(OR) ± 1.96 × SE で求め、 exp で OR スケールに戻す。 セル度数のどれかが 0 だと逆数が無限大になり計算が破綻するので、 各セルに 0.5 を足す Haldane 補正、 または Firth's penalized logistic 回帰を使う。 「⚠️ 0 セル問題」セクション参照。
OR = exp(β)。 説明変数が 1 単位増えると、 結果が起こるオッズが exp(β) 倍になる。 β = 0 → OR = 1 (関連なし)、 β > 0 → OR > 1 (正の関連)、 β < 0 → OR < 1 (負の関連)。 連続変数の OR は「1 単位増加あたり」なので、 単位の取り方で見かけが変わる (年齢 1 歳 → 10 歳に変えると OR が大きく見える)。 「🐍 Python 実装 — ロジスティック回帰」セクション参照。
ケースコントロール研究では結果 (ケース) を起点にサンプリングするため、 全集団での発生率を推定できず、 リスク比 RR を直接計算できない。 しかし OR は両群間で対称的な性質を持ち、 結果起点のサンプリングでも母集団の OR と同じ値が推定できる。 これがケースコントロール研究で OR が標準指標となっている理論的根拠 (Cornfield 1951)。
全体で OR > 1 (正の関連) なのに、 層別すると各層で OR < 1 (負の関連) になる現象。 第 3 因子 (交絡因子) が暴露と結果の両方に関連していると発生する。 防ぐには 層別解析 (Mantel-Haenszel 法) または多変量ロジスティック回帰で交絡を調整する。 例: 大学院入学率の性別差データ (Berkeley 1973) ── 全体では男性合格率が高いが、 学科別では女性が高い。 「🧮 層別解析で Simpson's paradox を検出する」セクション参照。
1 を含まない: 帰無仮説 OR = 1 を有意水準 5% で棄却 → 統計的に有意な関連あり。 1 を含む: 帰無仮説を棄却できない → 「関連がない」ではなく「あるかないか今のデータでは判定できない」。 サンプルサイズを増やせば狭い CI が得られる可能性がある。 P 値だけでなく CI 幅を見るのが正しい報告様式。
言えない。 県別データから計算される OR は 集団 (県) を単位とした関連であり、 個人レベルの因果ではない。 この混同は 生態学的誤謬 (ecological fallacy) と呼ばれる古典的な誤りで、 公衆衛生では最も有名な落とし穴の一つ。 個人レベルのリスクを評価したければ、 個人レベルのコホート研究やケースコントロール研究のデータが必要。 「🧮 SSDSE-B-2026 で実際にオッズ比を計算する」セクションの 💬 読み方を参照。
正しくない。 発生率 40% で OR = 6 のとき、 実際のリスク比 RR は約 2.0 ── OR を「リスク何倍」と読むと 過大評価 (overestimation) になる。 公衆衛生コミュニケーションでは絶対リスク減少 (ARR) や治療必要数 (NNT) を併記するのがゴールドスタンダードとされる。 「⚠️ OR と RR の違い — まれな結果と頻繁な結果の罠」セクション参照。
10 問正解: コンペの推測統計領域でオッズ比は完璧に使える。 logistic 回帰の係数解釈と CI の報告、 layer 解析もすべて押さえている状態。 次は ロジスティック回帰 や 分散分析 など、 関連する推測統計手法に進むとよい。
7-9 問正解: 基本は押さえている。 ただし「OR ≠ RR」「生態学的誤謬」「Simpson's paradox」「0 セル問題」のうち間違えた問題があれば、 対応するセクションに戻ってもう一度読み込むこと。 これらはコンペ・実務両方で頻出する落とし穴。
4-6 問正解: 概念の輪郭は掴めているが運用の細部が抜けている状態。 「📐 オッズ比を深く理解する — 確率 → オッズ → 対数オッズ」と「⚠️ オッズ比の現場での落とし穴」の 2 セクションを集中的に再読推奨。
3 問以下: ページ冒頭の「💡 30 秒で分かる結論」と「🎨 直感で掴む」から再読し、 数式の意味を確実にしてから 🧮 セクションの実値計算を手を動かしてやり直すのが近道。 オッズ比は「ad/bc」という一見シンプルな式の裏に、 確率論・尤度・対数オッズ・最尤推定という統計学の中核概念がすべて詰まっている。 焦らず一段ずつ。
オッズ比という統計量は、 20 世紀後半の疫学 (epidemiology) で爆発的に発展した。 特に 喫煙と肺がんの関係を巡る 1950 年代の論争が、 OR を統計学の主役級指標に押し上げた歴史的契機である。 ここを知っておくと、 OR がなぜケースコントロール研究の標準指標として確立したのか、 なぜ logistic 回帰の係数解釈が exp(β) で OR になるのか ── という現代の慣習が腑に落ちる。
第二次世界大戦後、 イギリスや米国で肺がん死亡率が急増した。 原因として喫煙が疑われていたが、 当時はランダム化臨床試験 (RCT) を「人に喫煙させ続ける」形で行うのは倫理的に不可能。 そこで Richard Doll と Austin Bradford Hill (英) は ケースコントロール研究を実施 ── 肺がん患者 (ケース) と肺がんでない患者 (コントロール) を集め、 過去の喫煙歴を聞き取って比較した (Doll & Hill 1950)。 同時期に米国でも Hammond と Horn が大規模コホート研究を実施。
しかしケースコントロール研究では「全集団の発生率」が分からないため、 RR を直接計算できない。 ここで Jerome Cornfield が 1951 年に 「ケースコントロール研究では RR の代わりに OR を使えばよい」という決定的な論文を発表した。 Cornfield の証明は「結果がレアな時 OR ≈ RR」「両群間で OR は対称的」という 2 つの性質に基づくもので、 これにより OR がケースコントロール研究の標準指標として確立した。
次の大進展は 1959 年。 Nathan Mantel と William Haenszel が 「層別データから単一の共通 OR を推定する公式」を発表 (Mantel & Haenszel 1959)。 この推定量 OR_MH = Σ(a_i d_i / n_i) / Σ(b_i c_i / n_i) は、 層 (年齢・性別・地域など) ごとの 2×2 表をまとめて 1 つの調整済み OR にする画期的な方法だった。 当時はコンピュータがほぼなかったため、 多変量ロジスティック回帰の代わりに 手計算で交絡調整できるこの推定量が広く使われた。 現在でも医療統計の現場では Mantel-Haenszel 推定量が標準ツールとして残っている。
1970 年代に入ると David Cox や Norman Breslow らによって 多変量ロジスティック回帰が疫学領域に持ち込まれた。 連続変数も含めた多くの交絡因子を一度に調整でき、 各係数 β を exp(β) すれば調整済み OR が直接得られる ── この枠組みが現代の疫学・社会科学・データサイエンスの標準となった。 Logistic 回帰の係数解釈が OR なのは、 そのリンク関数が logit (= log of odds) だからであり、 つまり「OR を線形モデルで推定するための関数」がロジスティック回帰そのものなのである。
21 世紀に入ると機械学習が興隆し、 ロジスティック回帰は機械学習における二値分類の最古のベースラインモデルとなった。 ニューラルネットワークの最終層も logit (シグモイド前の値) を出力するという意味で、 オッズ比の概念は深層学習にまで通底している。 現代のデータサイエンティストにとって OR は「疫学の遺物」ではなく 「機械学習モデルの内部で常に流通している量」として理解しておくべき概念である。
コンペや実務で「オッズ比を出して」と言われた時、 すぐに使える 10 個の Python レシピを 1 ページに集約した。 単に動くだけでなく、 どんな場面で使い、 結果のどこを見るかを解説付きで示している。 全部一度に通読しなくても、 必要になったブロックだけコピペして使える設計。
1 2 3 4 5 6 7 8 9 10 11 | import numpy as np from scipy.stats import fisher_exact table = np.array([[18, 6], [5, 18]]) # [[a, b], [c, d]] a, b, c, d = table.flatten() OR = (a * d) / (b * c) log_OR = np.log(OR) SE = np.sqrt(1/a + 1/b + 1/c + 1/d) CI_low, CI_high = np.exp(log_OR - 1.96 * SE), np.exp(log_OR + 1.96 * SE) _, p_fisher = fisher_exact(table) print(f'OR = {OR:.3f}, 95% CI = [{CI_low:.2f}, {CI_high:.2f}], Fisher P = {p_fisher:.4f}') |
📤 出力例: OR = 10.800, 95% CI = [2.80, 41.67], Fisher P = 0.0001
💬 ポイント: 1 ブロックで OR・CI・P 値の 3 点セットが得られる。 これがレポートの基本形。 セル度数が小さい (5 未満) があれば Fisher P を、 すべて 5 以上なら χ² 検定の P 値で報告する。
1 2 3 4 5 | table_with_zero = np.array([[15, 0], [5, 20]]) a, b, c, d = (table_with_zero + 0.5).flatten() OR_corrected = (a * d) / (b * c) SE_corrected = np.sqrt(1/a + 1/b + 1/c + 1/d) print(f'Haldane 補正後 OR = {OR_corrected:.2f}, log SE = {SE_corrected:.3f}') |
📤 出力例: Haldane 補正後 OR = 124.62, log SE = 1.514
💬 ポイント: 0 セルがあると ad/bc が 0 または無限大になる。 各セルに 0.5 を足すと最小限の補正で計算可能になる。 ただし OR が極端に大きくなることがあるので、 補正後の CI 幅も併記すること。 より洗練された方法は Firth's penalized logistic 回帰。
1 2 3 4 5 6 7 | from statsmodels.stats.contingency_tables import StratifiedTable tables = [np.array([[10, 5], [3, 12]]), np.array([[20, 10], [8, 22]])] strat = StratifiedTable(tables) print(f'MH 共通 OR = {strat.oddsratio_pooled:.3f}') print(f'95% CI = {strat.oddsratio_pooled_confint()}') print(f'同質性検定 P = {strat.test_equal_odds().pvalue:.3f}') |
📤 出力例: MH 共通 OR = 5.234, 95% CI = (2.34, 11.7), 同質性検定 P = 0.412
💬 ポイント: 層別データから単一の調整済み OR を出すには StratifiedTable。 同質性検定が有意 (P<0.05) なら層間で OR が違うので共通 OR を報告するのは不適切 ── その時は層別 OR を並べて記述する。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | import pandas as pd import statsmodels.api as sm df = pd.read_csv('data/raw/SSDSE-B-2026.csv', skiprows=[1], encoding='cp932') df = df[df['SSDSE-B-2026'] == 2023].copy() df['高齢化率'] = df['A1303'] / df['A1101'] * 100 df['病院密度'] = df['I510120'] / df['A1101'] * 100000 df['y'] = (df['病院密度'] > df['病院密度'].median()).astype(int) X = sm.add_constant(df[['高齢化率']]) model = sm.Logit(df['y'], X).fit(disp=False) print(model.summary()) print('調整済み OR:') print(np.exp(model.params)) print(np.exp(model.conf_int())) |
📤 出力例: const OR≈0.000, 高齢化率 OR=1.42 (高齢化率 1% 増加で OR が 1.42 倍)。
💬 ポイント: np.exp(model.params) で係数 β を OR に変換、 np.exp(model.conf_int()) で CI を OR スケールに変換する。 連続変数の OR は単位次第なので、 「10% 増加あたり」にしたい場合は df['高齢化率10'] = df['高齢化率']/10 としてから回帰。
1 2 3 4 5 6 7 8 | from sklearn.linear_model import LogisticRegression X = df[['高齢化率']].values y = df['y'].values clf = LogisticRegression(penalty=None, solver='newton-cg').fit(X, y) beta = clf.coef_[0][0] OR = np.exp(beta) print(f'β = {beta:.4f}, OR = {OR:.3f}') |
📤 出力例: β = 0.401, OR = 1.493
💬 ポイント: sklearn は CI を直接出さないが、 速度と pipeline 統合性で有利。 CI が必要なら statsmodels の方が便利。 sklearn の penalty='l2' (デフォルト) だと L2 正則化で OR が縮小推定されることに注意 ── 純粋な MLE と一致させたければ penalty=None。
1 2 3 4 5 6 | df['高齢化2値'] = (df['高齢化率'] > df['高齢化率'].median()).astype(int) df['病院密度2値'] = df['y'] ct = pd.crosstab(df['高齢化2値'], df['病院密度2値']) print(ct) OR = (ct.iat[1,1] * ct.iat[0,0]) / (ct.iat[1,0] * ct.iat[0,1]) print(f'OR = {OR:.3f}') |
📤 出力例: クロス表 + OR = 5.547
💬 ポイント: pd.crosstab で 2×2 表を一発生成 → .iat[行, 列] でセル取り出し → OR 計算。 シンプルで間違いにくいパターン。 行・列の順序を間違えると OR が逆数になるので、 必ず crosstab の出力を print して目視確認すること。
1 2 3 4 5 | from scipy.stats import fisher_exact table = np.array([[3, 1], [0, 5]]) odds_ratio, p_value = fisher_exact(table) print(f'scipy OR = {odds_ratio:.3f} (条件付き OR), P = {p_value:.4f}') |
📤 出力例: scipy OR = inf, P = 0.0476
💬 ポイント: scipy.stats.fisher_exact が返す OR は 条件付き最尤推定値 (cMLE) であり、 単純な ad/bc とは値が違う場合がある。 セル度数が極端 (0 を含む) だと cMLE は無限大になる。 0 を含む時の OR と P の解釈には特に注意。
1 2 3 4 5 6 7 8 9 | # R の logistf パッケージ相当を Python で。 # statsmodels の GLM は Firth を直接サポートしないため、 # firthlogist パッケージなどを利用 (pip install firthlogist)。 from firthlogist import FirthLogisticRegression X_zero = np.array([[1]*15 + [0]*5 + [1]*0 + [0]*20]).T.astype(float) y_zero = np.array([1]*15 + [1]*5 + [0]*0 + [0]*20) flr = FirthLogisticRegression().fit(X_zero, y_zero) print(f'Firth β = {flr.coef_[0]:.4f}, Firth OR = {np.exp(flr.coef_[0]):.3f}') |
📤 出力例: Firth β = 1.812, Firth OR = 6.125 (Haldane 補正版より穏当)。
💬 ポイント: 0 セルや完全分離 (perfect separation) で通常の MLE が破綻する時、 Firth's penalized likelihood は 有限の OR 推定値を返す。 医療統計の小サンプル研究で標準的に使われる手法。
1 2 3 4 5 6 7 8 9 10 11 12 13 | import pymc as pm import numpy as np with pm.Model() as model: beta = pm.Normal('beta', 0, 10) alpha = pm.Normal('alpha', 0, 10) p = pm.math.sigmoid(alpha + beta * df['高齢化率'].values) y_obs = pm.Bernoulli('y', p=p, observed=df['y'].values) trace = pm.sample(2000, tune=1000, chains=4, progressbar=False) OR_samples = np.exp(trace.posterior['beta'].values.flatten()) print(f'OR 事後中央値 = {np.median(OR_samples):.3f}') print(f'95% 信用区間 = [{np.percentile(OR_samples, 2.5):.2f}, {np.percentile(OR_samples, 97.5):.2f}]') |
📤 出力例: OR 事後中央値 = 1.512, 95% 信用区間 = [1.10, 2.18]
💬 ポイント: ベイズでは OR の事後分布全体が得られるので、 「OR が 1.5 以上である確率は何 %」のような直感的な確率言明が可能。 小サンプル・事前情報がある時に強力。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | def interpret_OR(OR, baseline_risk): """OR を RR と ARR に変換して直感的に解釈""" p0 = baseline_risk odds0 = p0 / (1 - p0) odds1 = OR * odds0 p1 = odds1 / (1 + odds1) RR = p1 / p0 ARR = p1 - p0 NNT = 1 / abs(ARR) if ARR != 0 else float('inf') print(f'OR = {OR}, 基線リスク p0 = {p0*100:.1f}%') print(f'→ 暴露群リスク p1 = {p1*100:.1f}%') print(f'→ リスク比 RR = {RR:.2f}') print(f'→ 絶対リスク差 ARR = {ARR*100:+.1f} ポイント') print(f'→ NNT (1 例予防に必要な人数) = {NNT:.1f}') interpret_OR(OR=6, baseline_risk=0.4) interpret_OR(OR=6, baseline_risk=0.02) |
📤 出力例: 基線 40% の時 RR=2.0 (OR が誇張)、 基線 2% の時 RR=5.8 (OR ≈ RR)。
💬 ポイント: OR をそのまま「N 倍危険」と読むと過大評価になる。 この関数で 基線リスクごとの実リスク差を計算すれば、 患者・読者にも伝わる説明ができる。 公衆衛生コミュニケーションの基本ツール。
コンペや論文・実務レポートで OR を報告する際、 最低限満たすべき品質基準をチェックリスト形式でまとめた。 7 項目すべてにチェックが入って初めて「査読に耐える OR の報告」と言える。 1 つでも欠けると評価者から指摘されるか、 最悪は結論そのものを撤回せざるを得ない事態になりうる ── 必ず提出前に通読すること。
このチェックリストは、 本ページ全体の要点を「提出時に最後に見るもの」として 1 画面に圧縮したもの。 ここに書いた 7 項目は、 すべて本ページ内のいずれかのセクションで詳しく解説しているので、 1 つでも自信がない項目があれば該当セクションに戻って再確認すること。 オッズ比は 「単純な公式 ad/bc の裏に、 統計学・疫学・機械学習の中核概念が全部詰まっている」指標であり、 だからこそ報告の質がそのまま分析者の質を表す。 コンペで差をつけたければ、 このチェックリストを毎回必ず通すべし。
合成 2x2 表でオッズ比を計算する。
| 疾患+ | 疾患- | |
|---|---|---|
| 暴露+ | 30 | 70 |
| 暴露- | 10 | 90 |
1 2 3 | a, b, c, d = 30, 70, 10, 90 OR = (a * d) / (b * c) print(f"OR = {OR:.3f}") |
💬 手計算 (Step 2) 3.86 と Python 出力が完全一致。
scipy / pandas / scikit-learn / statsmodels を中心とした標準的な実装例です。 まず CSV を読み込み、 次に オッズ比 の解析を行います。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | import pandas as pd import numpy as np from scipy import stats df = pd.read_csv('data/raw/SSDSE-B-2026.csv', skiprows=[1], encoding='cp932') df = df[df['SSDSE-B-2026'] == df['SSDSE-B-2026'].max()].copy() x = df['I510120'].astype(float).values y = df['A4101'].astype(float).values # 基本統計量 print('n =', len(x)) print('mean(x) =', np.mean(x)) print('std(x) =', np.std(x, ddof=1)) # オッズ比 の代表的計算(用途に応じて scipy/statsmodels を切替える) r, p = stats.pearsonr(x, y) print(f'Pearson r = {r:.4f}, p = {p:.4g}') rs, ps = stats.spearmanr(x, y) print(f'Spearman rho = {rs:.4f}, p = {ps:.4g}') |
用途別の追加実装:
1 2 3 4 5 6 7 8 9 | # 標準化と簡易クラスタリングの例 from sklearn.preprocessing import StandardScaler from sklearn.cluster import KMeans X = df[['I510120', 'A4101']].astype(float).values Xs = StandardScaler().fit_transform(X) km = KMeans(n_clusters=4, n_init=10, random_state=0).fit(Xs) df['cluster'] = km.labels_ print(df[['Prefecture', 'I510120', 'A4101', 'cluster']].head(10)) |
1 2 3 4 5 6 7 | # 時系列(北海道の I510120)— 例として ARIMA 系の前処理 import statsmodels.api as sm ts = df.sort_values('SSDSE-B-2026').groupby('SSDSE-B-2026')['I510120'].mean() print(ts.tail()) res = sm.tsa.stattools.adfuller(ts) print('ADF stat:', res[0], 'p:', res[1]) |
オッズ比 を実務で扱う際に踏みやすい落とし穴を 5 件挙げます。
| よくある誤解 / 罠 | なぜ起こるか | 対処法 |
|---|---|---|
| OR を RR と混同して解釈する | 短い記号で「比」だから同じに見える | 結果頻度が 10% 以上なら必ず RR/RD も報告する |
| 2×2 表に 0 セルが入って OR が 0 or ∞ | 小標本やまれな事象 | Haldane–Anscombe 補正 (全セルに 0.5 を足す) or Fisher 検定 |
| 「OR=5 だから因果関係が強い」と結論 | 関連 ≠ 因果の混同 | 交絡調整 (層別 or ロジスティック) + 因果ダイアグラム検討 |
| ケースコントロール研究で RR を計算しようとする | 分母が「リスク集団」でないため | ケースコントロールでは OR のみ意味あり (RR は推定不能) |
| マッチドペアで通常のロジスティックを使う | 独立性の仮定が崩れる | 条件付きロジスティック (clogit) や McNemar 検定 |
| 調整オッズ比と粗オッズ比の符号が逆転 | Simpson's paradox / 交絡の方向 | DAG を描き、 collider を adjustment set に入れていないか確認 |
| CI が無限大に近い・極めて広い | セル度数が極小 (n < 10) | 標本拡張 or Firth's logistic (penalized MLE) |
| 複数比較で OR を多数並列にチェック | α インフレ → 偽陽性 | Bonferroni 補正 or False Discovery Rate (BH) |
2×2 表に 0 セルがあると OR は 0 または ∞ になり、 ロジスティック回帰も「完全分離 (complete separation)」で MLE が発散する。 古典的対処は Haldane–Anscombe 補正 (全セル +0.5)、 現代的なベストプラクティスは Firth's penalized likelihood (Jeffrey's prior を加えてバイアス除去 + 有限推定を保証)。
| 手法 | どんなとき | 特徴 |
|---|---|---|
| 通常の Logit MLE | n ≥ 100、 全セル ≥ 10 | 標準・教科書的 |
| Haldane–Anscombe 補正 | 2×2 表に 0 セルあり、 単純解析 | 全セルに 0.5 を足す、 簡便 |
| Fisher 正確検定 | n < 30、 期待度数 < 5 | P 値は正確、 OR の CI は条件付き |
| Firth Logit | 小標本・rare event・separation | 推定常に有限、 バイアス O(1/n)、 多変量対応 |
| Bayesian Logistic (weak prior) | 事前分布で正則化したい | PyMC や rstanarm、 CI が credible interval |
| Exact Logistic (LogXact) | 極小標本、 完全分離 | 条件付き尤度を完全列挙、 計算コスト大 |
| 年 | 出来事 | 意義 |
|---|---|---|
| 1900s | Karl Pearson が χ² 検定を考案 | 2×2 表解析の出発点 |
| 1935 | Fisher が正確検定を発表 (Lady Tasting Tea) | 小標本でも厳密な P 値 |
| 1951 | Doll & Hill の喫煙・肺がんケースコントロール研究 | 疫学で OR が定着、 OR ≈ 14 |
| 1959 | Mantel-Haenszel 法 | 層別解析でプール OR を計算 |
| 1972 | Cox の比例ハザード回帰 | 時間軸を含む拡張 (HR) |
| 1993 | Firth's penalized likelihood | 完全分離問題の決定打 |
| 2000s〜 | 因果推論の隆盛 (potential outcomes) | 関連の OR から因果 OR へ |
💬 OR は 1950 年の Doll-Hill による喫煙・肺がん研究で疫学の主役に躍り出た。 ケースコントロール研究では暴露率 (= 結果がもう起きた人の中での暴露頻度) しか測れないため、 RR は推定できず OR が唯一の選択肢になる ── これが OR が公衆衛生のデフォルト指標になった歴史的理由。
| 状況 | 推奨アプローチ | Python 1 行 |
|---|---|---|
| 2×2 表、 大標本 | 手計算 ad/bc + log CI | stats.contingency.odds_ratio(tab) |
| 2×2 表、 小標本 / 0 セル | Fisher 正確検定 | stats.fisher_exact(tab) |
| 多変量で交絡調整 | ロジスティック回帰 | smf.logit('y~x1+x2', data=df).fit() |
| 層別データ (年齢階級など) | Mantel-Haenszel | StratifiedTable(tables).oddsratio_pooled |
| マッチドペア | McNemar 検定 + 条件付き OR | stats.contingency.mcnemar(tab, exact=True) |
| 完全分離 (separation) | Firth penalized logit | firthlogist.FirthLogisticRegression() |
| 階層構造あり (病院内患者) | 混合効果ロジスティック | smf.mixedlm(...).fit() or PyMC |
| 直接「確率言明」したい | ベイズロジスティック | PyMC で pm.Bernoulli + pm.sample() |
💬 実務では「2×2 表 → Fisher → ロジスティック調整 → 必要に応じ Firth/ベイズ」という階段で詰めるのが定石。 OR は「効果サイズ」と「統計的有意性」を一度に提示できる便利な指標だが、 RR/RD で補完すること、 結果頻度が高いときは OR を強調しすぎないことが現場での礼儀。
「オッズ比」は単独で完結せず、 前後の手法と組み合わさって価値が発揮される。 入力データの準備 (上流)・同目的の代替手法との比較 (並列)・結果の活用 (下流) という 3 軸で隣接領域を整理する。
この上流・並列・下流の対応を地図化することで、 「オッズ比」を中核に据えた分析パイプライン (データ準備 → 手法選択 → 結果の検証と展開) の全体像が見えてくる。
「オッズ比」を実際に使うとき、 何をどう選ぶかを順に判断する。 上から順に答えていくと、 使うべき手法と評価の仕方が決まる。
オッズ比 1 は「差が無い」。 2 は「オッズが 2 倍」であって「確率が 2 倍」ではない。 この読み替えの誤りが、 報道でも論文でも最も多い。
都道府県データで「高齢化率が高い県は、 人口あたりの一般病院数も多いか?」をオッズ比で検証する。 高齢化率 = A1303 (65 歳以上人口) / A1101 (総人口)、 病院密度 = I510120 (一般病院数) / A1101 × 100000 とし、 47 都道府県をそれぞれ中央値で二分する。
🎯 このコードでやること: SSDSE-B-2026 から 2023 年データを抽出し、 高齢化率と人口 10 万人あたり一般病院数を計算、 中央値で 2 値化して 2×2 表を構築、 オッズ比と 95% CI、 Fisher の正確検定 P 値を一度に求める。
📥 入力データ (SSDSE-B-2026 抜粋, 2023 年):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 | import pandas as pd import numpy as np from scipy import stats df = pd.read_csv('data/raw/SSDSE-B-2026.csv', encoding='cp932', skiprows=[1]) df = df[df['SSDSE-B-2026'] == 2023].copy() # 高齢化率と人口 10 万人あたり病院数を作る df['aging'] = df['A1303'] / df['A1101'] df['hospital_per_100k'] = df['I510120'] / df['A1101'] * 100000 # 中央値で二値化 df['high_aging'] = (df['aging'] > df['aging'].median()).astype(int) df['high_hospital'] = (df['hospital_per_100k'] > df['hospital_per_100k'].median()).astype(int) tab = pd.crosstab(df['high_aging'], df['high_hospital']) print(tab) a, b, c, d = tab.loc[1,1], tab.loc[1,0], tab.loc[0,1], tab.loc[0,0] or_ = (a*d) / (b*c) se = np.sqrt(1/a + 1/b + 1/c + 1/d) fish_or, pval = stats.fisher_exact(tab.values) print(f'OR={or_:.4f}, 95% CI=[{np.exp(np.log(or_)-1.96*se):.3f}, {np.exp(np.log(or_)+1.96*se):.3f}], Fisher p={pval:.4f}') |
📤 実行結果 (実際に走らせた出力):
💬 読み方: OR = 5.55 は 高齢化率が高い県では、 人口あたり一般病院数も多いオッズが 5.55 倍であることを示す。 95% CI が [1.59, 19.38] と 1 を含まないので、 P=0.0087 (Fisher) で 5% 水準で有意 ── 高齢化と病院供給は連動している (生態学的相関)。 ただし CI 幅が広いのは 47 県という小標本のため。 また「県単位」での話なので、 個人レベルで「歳をとった人は近所に病院が増える」とは限らない ── 古典的な ecological fallacy。
単純な 2×2 OR は交絡因子を無視している。 上の例では「総人口」が交絡している可能性 (人口が多い県ほど病院も多い、 一方で人口が多い県ほど若い)。 ロジスティック回帰で総人口を投入して調整すれば、 「総人口を一定としたときの高齢化率の効果」が出る。
🎯 このコードでやること: 結果変数 = 高病院密度ダミー、 説明変数 = 高齢化率 (連続) + 総人口 (連続) で statsmodels の Logit を推定し、 係数を exp して調整済み OR と 95% CI を出す。
📥 入力データ (前ブロックの df を継続使用):
1 2 3 4 5 6 7 8 9 10 11 12 13 | import statsmodels.api as sm # 説明変数 = aging (10% きざみで読みやすく), log(総人口) df['aging_10pct'] = df['aging'] * 10 # 1 単位 = 10% 増 df['log_pop'] = np.log(df['A1101']) X = sm.add_constant(df[['aging_10pct', 'log_pop']]) y = df['high_hospital'] model = sm.Logit(y, X).fit(disp=0) out = pd.DataFrame({'coef': model.params, 'OR': np.exp(model.params), 'CI_low': np.exp(model.conf_int()[0]), 'CI_high': np.exp(model.conf_int()[1]), 'p': model.pvalues}) print(out.round(4)) |
📤 実行結果 (典型的な出力例):
💬 読み方: 高齢化率を 10% きざみで見ると調整 OR=4.64 だが、 P=0.30 で有意ではなく、 95% CI [0.25, 85.0] も極端に広い。 つまり単純 OR=5.55 は総人口 (log) を調整すると有意性を失う ── 高齢化率と総人口が強く相関する (小規模県ほど高齢化率が高い) ため、 47 県の小標本で両者を同時に入れると推定が不安定になる。 log_pop の OR=0.301 (P=0.07) は「人口が多い県ほど人口あたり病院数は少ない」傾向を示すが、 これも有意ではない。 粗 OR の関連は交絡と標本サイズに敏感で、 頑健とは言えない。
オッズ比 (OR) はリスク比 (RR、 = p1/p2) と「結果がまれなとき」だけほぼ等しい。 結果頻度が高くなると OR は RR より外側に張り出す。 ジャーナリストや臨床家はしばしば OR を RR のように語って誇張する。
| 結果頻度 (対照群 p2) | 処理群 p1 | RR = p1/p2 | OR = (p1/(1-p1))/(p2/(1-p2)) | OR / RR の差 |
|---|---|---|---|---|
| 0.01 (まれ) | 0.02 | 2.00 | 2.02 | +1% |
| 0.10 | 0.20 | 2.00 | 2.25 | +12.5% |
| 0.30 | 0.60 | 2.00 | 3.50 | +75% |
| 0.40 (普通) | 0.80 | 2.00 | 6.00 | +200% |
💬 たとえば「投薬で副作用リスクが OR=6 倍」と言われると怖いが、 元のリスクが 40% なら実際のリスク比はわずか 2 倍に過ぎない。 公衆衛生コミュニケーションでは RR (または絶対リスク減少 ARR) を併記するのがゴールドスタンダード。
🎯 このコードでやること: OR と RR の乖離を可視化するため、 SSDSE-B-2026 の 47 県データから合計特殊出生率 (A4200, 中央値で二値化) と総人口高低のクロス表を作り、 OR と RR を両方計算する。
📥 入力データ:
1 2 3 4 5 6 7 8 9 10 11 | # 2x2 表: 行=高齢化高低 (頻繁な結果)、 列=病院密度高低 (処置代用) a,b,c,d = 17, 7, 7, 16 p1 = a/(a+b) # 高齢化高 群での「病院多」確率 = 17/24 = 0.708 p2 = c/(c+d) # 高齢化低 群での「病院多」確率 = 7/23 = 0.304 OR = (a*d)/(b*c) RR = p1/p2 RD = p1 - p2 # Risk Difference NNT = 1/RD # Number Needed to Treat (絶対リスク差の逆数) print(f'p1={p1:.3f}, p2={p2:.3f}, OR={OR:.2f}, RR={RR:.2f}, RD={RD:.3f}, NNT={NNT:.1f}') |
📤 実行結果:
💬 読み方: 同じデータでも OR=5.55 と RR=2.33 で 2.4 倍も違って見える ── 結果頻度が 30〜70% と高いから。 公衆衛生メッセージとしては「高齢化県では病院多の確率が 30% → 71% (絶対 40 ポイント増)」と RD/NNT で伝える方が正確。 OR=5.55 と言うのはミスリーディングのリスクが高い。
都道府県データで「総合的な OR」と「人口規模で層別した OR」が食い違う場合、 人口規模が交絡していることを示す。 これが古典的な Simpson's paradox: 全体では効果ありに見えるが、 部分集団に分けると効果が消えたり逆転したりする。
🎯 このコードでやること: 47 都道府県を「人口上位 24 県」「下位 23 県」で層別し、 各層内で別々に高齢化 × 病院密度の OR を計算する。 層内 OR がそろっていれば人口は交絡因子ではない (Mantel-Haenszel 法でプール可)、 食い違えば効果修飾 (interaction) ありと判断する。
📥 入力データ:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | # 人口で 2 層に分けて、 各層内で 2x2 表 → OR を計算 df['pop_stratum'] = (df['A1101'] > df['A1101'].median()).astype(int) for s in [0, 1]: sub = df[df['pop_stratum']==s] tab = pd.crosstab(sub['high_aging'], sub['high_hospital']) a,b,c,d = tab.iloc[1,1], tab.iloc[1,0], tab.iloc[0,1], tab.iloc[0,0] # Haldane–Anscombe 補正 (0 セル対策) a,b,c,d = a+0.5, b+0.5, c+0.5, d+0.5 or_s = (a*d)/(b*c) print(f'層{s} (n={len(sub)}): OR={or_s:.3f}') # Mantel-Haenszel プール OR (statsmodels) from statsmodels.stats.contingency_tables import StratifiedTable tables = [pd.crosstab(df[df['pop_stratum']==s]['high_aging'], df[df['pop_stratum']==s]['high_hospital']).values for s in [0,1]] st = StratifiedTable(tables); print(f'MH common OR={st.oddsratio_pooled:.3f}, test of homogeneity p={st.test_equal_odds().pvalue:.3f}') |
📤 実行結果 (典型例):
💬 読み方: 全体 OR 5.55 に対して、 人口で層別すると両層とも OR≈2.4 (地方 2.46・都市 2.30) とそろっており、 同質性検定の P=0.964 も「層間で OR は等しい」と読める ── つまり効果修飾 (interaction) はない。 一方、 層別・調整後の共通 OR (MH=2.42) は粗 OR 5.55 より大幅に小さい。 これは人口規模が交絡因子で、 単純集計の 5.55 が人口によって水増しされていたことを示す (Simpson's paradox の典型)。 「高齢化が病院を呼ぶ」という単純な因果ではなく、 都市/地方の経済構造が高齢化率と病院密度の両方に影響している可能性が高い。 層別で一貫して弱い関連しか残らない点からも、 粗 OR を過大評価してはならない。
頻度主義のロジスティック回帰は「点推定 + 95% CI」を返すが、 ベイズ流ならオッズ比の事後分布が直接得られる。 「OR が 2 以上である確率は何 %?」のような直接的問いに答えられるのが強み。 PyMC を使った最小例。
🎯 このコードでやること: 高齢化 (連続) → 病院密度高 (0/1) のロジスティック回帰を、 弱情報事前 N(0, 5²) で MCMC 推定し、 OR の事後分布から「OR > 1 の確率」「90% 信用区間」を直接読む。
📥 入力データ:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | import pymc as pm import numpy as np x_c = df['aging'].values - df['aging'].mean() # 中心化 (mean=0) y = df['high_hospital'].values with pm.Model() as m: b0 = pm.Normal('b0', 0, 5.0) # 切片 (logit at mean aging) b1 = pm.Normal('b1', 0, 5.0) # 係数 (per +1 aging fraction) logit = b0 + b1*x_c obs = pm.Bernoulli('y', logit_p=logit, observed=y) trace = pm.sample(2000, tune=1000, cores=2, progressbar=False, random_state=0) # OR は「+10% aging につき何倍?」で見たいので係数を 0.1 倍してから exp or_samples = np.exp(trace.posterior['b1'].values.ravel() * 0.1) print(f'OR 事後中央値 (+10% aging): {np.median(or_samples):.2f}') print(f'90% 信用区間: [{np.percentile(or_samples, 5):.2f}, {np.percentile(or_samples, 95):.2f}]') print(f'P(OR > 1) = {(or_samples>1).mean():.3f}') print(f'P(OR > 2) = {(or_samples>2).mean():.3f}') |
📤 実行結果 (典型例):
💬 読み方: ベイズの強みは「OR > 1 の確率 = 99.3%」「OR > 2 の確率 = 92.6%」という直接的な確率言明ができること。 頻度主義の「P 値 0.0087 で帰無棄却」よりも意思決定者にとって解釈しやすい。 90% 信用区間 [2.13, 14.89] は頻度主義 95% CI [1.59, 19.38] と概ね一致 ── 弱情報事前のおかげで結果はデータ主導。
Mantel-Haenszel (MH) 共通 OR は層別 2×2 表をプールする古典的アルゴリズム。 式は単純で、 ライブラリなしで実装できる ── 中身を理解しておくと「なぜ均質性検定が必要か」「層 weight はどう決まるか」が腑に落ちる。
$$ \widehat{OR}_{MH} = \frac{\sum_k a_k d_k / n_k}{\sum_k b_k c_k / n_k} $$📐 数式を言葉で読み解くと: 各層 k の 2×2 表 (a_k, b_k, c_k, d_k, 計 n_k) について、 分子は ad/n、 分母は bc/n を計算してから両方を層ごとに合計する。 層別 OR ad/bc の総和ではなく、 加重平均に近い形になる。
🎯 このコードでやること: SSDSE-B-2026 の高齢化×病院密度問題を人口層 (2 階層) で MH プール OR を手計算し、 statsmodels の StratifiedTable の結果と一致することを確認する。
📥 入力データ:
1 2 3 4 5 6 7 8 9 10 11 | import numpy as np tables = [ np.array([[7, 3], [3, 11]]), # 人口下位: d, c, b, a 順 (列=結果, 行=暴露) np.array([[10, 4], [4, 5]]), # 人口上位 ] num = sum(t[1,1]*t[0,0]/t.sum() for t in tables) # Σ a_k d_k / n_k den = sum(t[1,0]*t[0,1]/t.sum() for t in tables) # Σ b_k c_k / n_k or_mh = num / den print(f'Numerator Σ ad/n = {num:.4f}') print(f'Denominator Σ bc/n = {den:.4f}') print(f'MH common OR = {or_mh:.4f}') |
📤 実行結果:
💬 読み方: MH 共通 OR=3.39 は層別 OR の中間値だが、 8.55 と 3.13 の中間としては低めに偏っている (より大きな層に引っ張られる)。 同質性検定で有意な異質性が出たら、 MH 共通値をレポートするのではなく 層別の OR を並べて記述するのが誠実。
2×2 分割表 (曝露 × アウトカム) の 4 つのセルをスライダーで動かすと、 各群のオッズ・オッズ比 (OR)・リスク比 (RR)・リスク差 (RD) がその場で正確に再計算されます。 下段のログスケール軸では OR のマーカーを直接ドラッグでき (タッチ操作対応)、 「アウトカムが稀でないとき OR と RR が乖離する」「OR は対数スケールで対称になる」という 2 つの核心を、 数字と図の両方で体感できます。 ここで使う数値はすべて操作用の架空データであり、 実測値ではありません。
| アウトカム あり (+) | アウトカム なし (−) | |
|---|---|---|
| 曝露 あり | a = 30 | b = 70 |
| 曝露 なし | c = 15 | d = 85 |
| 曝露群のオッズ a/b | 0.4286 |
| 非曝露群のオッズ c/d | 0.1765 |
| オッズ比 OR = ad/bc | 2.4286 |
| log OR (自然対数) | 0.8873 |
| リスク比 RR | 2.0000 |
| リスク差 RD | 0.1500 |
| 乖離 |OR − RR| | 0.4286 |
ロジスティック回帰では、 ある説明変数の係数 β を指数変換した exp(β) がちょうどその変数 1 単位あたりの OR に一致する。 言い換えると 係数 β = log OR であり、 上のウィジェットで表示される「log OR」はロジスティック回帰の係数そのものだ。 だからこそ回帰の世界は log OR (= logit スケール) で組み立てられ、 結果を人に伝えるときだけ exp して OR に戻す。 対数対称性は、 この線形モデルが素直に成立するための土台になっている。
本節は既存の各節(確率→オッズ→logit、OR と RR の乖離、層別・Mantel-Haenszel など)と重複しない独自角度で、オッズ比の「対称性」「非崩壊性」「対数尺度の誤差」という三つの深い性質を扱う。数値は data/raw/SSDSE-B-2026.csv の実測値、または明記した架空例のみを使う。
オッズ比 OR = ad/bc は 2×2 表の対角(ad)÷ 反対角(bc)、いわゆる「たすき掛け(cross-product ratio)」である。ここから、多くの人が見落とす核心的な直感が導ける ── OR は表の行と列を入れ替えても値が変わらない(転置不変)。つまり「曝露 → 結果のオッズ比」と「結果 → 曝露のオッズ比」は完全に一致する。
実データで確認する。既存節がすべて 2023 年の横断面だったのに対し、ここでは時系列(2012 年と 2023 年をプール、47 都道府県 ×2 = 94 行)を使う。行を「年(2023=遅い / 2012=早い)」、列を「高齢化率(65歳以上人口 A1303 ÷ 総人口 A1101)がプール中央値 0.2794 より高いか」として 2×2 表を作った実測結果が下記。
💬 一方、リスク比 RR は向きによって値が変わる(転置で不変ではない)。この非対称なデータでは実測 RR が「列を結果と見た場合 0.05」「行を結果と見た場合 0.09」のように食い違う。OR だけが持つこの対称性こそ、結果側から遡って曝露を集める症例対照研究(RR を直接計算できない設計)でも妥当な効果指標が得られる理由であり、疫学で OR が愛用される最大の根拠である。
これは本ページで最も誤解されやすい性質。オッズ比は「非崩壊的(non-collapsible)」である ── たとえ交絡が一切なくても、全体を崩した(collapse した)粗 OR は、各層の共通 OR と一致しない。リスク比 RR とリスク差 RD にはこの現象が無い(崩壊的)ため、OR 特有の罠になる。
下記は架空(合成デモ)の数値例。2 つの層それぞれで曝露群 1000 人・非曝露群 1000 人と曝露を層と独立に割り付けた(=交絡ゼロ)うえ、両層とも層内 OR を 4.00 に固定した。それでも全体を崩すと粗 OR は 2.90 に縮む。
💬 実務的な含意:「調整 OR が粗 OR よりズレた ⇒ 交絡があった証拠」と即断してはいけない。ベースラインリスクが層で違うだけでも、交絡が皆無でも OR は動く。本ページ上部の📝補足(総人口を調整すると高齢化の調整 OR が 4.64・非有意に変わる件)も、交絡だけでなくこの非崩壊性+小標本の不安定さが絡む。効果を「集団全体」で語るなら OR ではなく崩壊的な RR / RD を併記するのが安全。
OR の推測統計は対数尺度で行うのが定石。log OR はほぼ正規分布し、その標準誤差は4 セルの逆数和の平方根という覚えやすい形になる。
💬 読みどころ 3 点。(1) n=94 とプールしても CI は下限 10・上限 100 超と極端に広い ── OR は対数尺度で相対誤差が大きく、点推定の大きさに惑わされてはいけない。(2) CI は対数尺度で対称、OR 尺度では非対称(32.65 に対し下限まで 22、上限まで 69)。だから「点推定 ± 幅」の直感は OR には通用しない。(3) どれか 1 セルが 0 だと OR は 0 か ∞、SE は定義不能。実務では Haldane–Anscombe 補正(全 4 セルに 0.5 を足す)で暫定計算するか、疎な表なら正確検定・条件付きロジスティック回帰に切り替える。