
Simpsonのパラドックスを300人分シミュレーションしたら、グループ別r=0.55/0.82がプールでr=-0.20に反転した
「グループごとに見ると両方とも右肩上がりなのに、全部まとめると右肩下がりになる」——そんなことが起こり得る。Simpsonのパラドックスと呼ばれる現象で、部分集団それぞれの傾向と、それらを合算した全体の傾向が、正反対を向いてしまうことがある。
言葉で聞くと「そんな都合の良いことが起きるのか」と半信半疑になる話なので、コンドルセ陪審定理の記事のときと同じように、実際に自分でデータを作って手を動かして確かめてみることにした。あちらは「多数決という集約が個人の実力差を増幅する」話だったが、今回は「平均や相関という集約が、下部構造の傾向を丸ごと覆い隠す・反転させる」話になる。
実験に使ったコードの全文はGitHubに置いています。
シナリオ: 学習アプリの利用時間とテストスコア
架空の学習アプリを想定する。ユーザーには2つのグループがいる。
- 上級者グループ: もともと実力があるので、アプリを使う時間は少なめ(週2〜3時間程度)。それでも到達テストのスコアは高め
- 初級者グループ: 追いつこうとしてアプリを長時間使う(週13時間前後)。ただし基礎点が低いので、スコアはまだ上級者に及ばない
ただしどちらのグループの内部でも、「アプリを使えば使うほどスコアが伸びる」という本物の効果(真の傾き)は共通して仕込んである。これをnumpyで生成した。
SLOPE = 3.5 # 真の効果: 週1時間の利用でスコア+3.5点(グループ内共通)
NOISE_SD = 6.0
# 上級者: 少ない利用時間・高い基礎点
hours_a = clip(rng.normal(3.0, 1.2, 150), 0.2, None)
score_a = clip(60.0 + SLOPE * hours_a + rng.normal(0, NOISE_SD, 150), 0, 100)
# 初級者: 多い利用時間・低い基礎点
hours_b = clip(rng.normal(13.0, 2.5, 150), 0.2, None)
score_b = clip(15.0 + SLOPE * hours_b + rng.normal(0, NOISE_SD, 150), 0, 100)
スコアはテストの点数なので0〜100点にクリップしている(ただしこの主実験の設定では、クリップに引っかかった点は1つもない。クリップが実際に効いてくるのは後半の掃引で基礎点差を極端に大きくしたときだけだ)。seed=42で1回サンプリングした実測値はこうなった。
上級者(n=150): 利用時間 平均2.94時間(sd=1.02) スコア 平均70.10(sd=7.17)
初級者(n=150): 利用時間 平均13.07時間(sd=2.49) スコア 平均60.44(sd=10.86)
グループ内では正の相関、プールすると負に反転した
まずグループごとに、利用時間とスコアの回帰直線とPearsonの相関係数を計算する。
上級者の回帰: score = 58.76 + 3.856 * hours r = 0.5508 (p=2.83e-13)
初級者の回帰: score = 13.61 + 3.584 * hours r = 0.8210 (p=7.60e-38)
どちらのグループも、利用時間が増えるほどスコアが上がるという明確な正の相関(r=0.55、r=0.82)で、しかも1時間あたりの効き目(傾き3.856・3.584)もほぼ同じくらいの大きさになっている。ここまでは仕込んだ通りで、意外性は何もない。
ところが、この300人分のデータを上級者・初級者の区別を外していきなり1本の回帰にかけると、結果はこうなる。
プール回帰: score = 68.37 + -0.388 * hours r = -0.2021 (p=4.28e-04)
相関の符号がまるごと反転した。 しかもp=4.28e-04と、偶然のブレでは片付けられない水準ではっきり負に出ている。「グループ内ではどちらも利用時間がスコアを押し上げているのに、まとめて見ると利用時間が長い人ほどスコアが低い」という、実務で見たら普通に誤読しそうな図になる。

種明かしをすると単純で、上級者クラスタは「左上(利用時間少・スコア高)」、初級者クラスタは「右下(利用時間多・スコア低)」に位置していて、この2つの塊を結ぶ向きがグループ内の傾きとは逆を向いている。プールした回帰直線は、グループ内の”本物の効果”ではなく、この2つの塊の位置関係にほぼ支配されてしまう。個々の点のばらつきを見れば正の傾きなのに、集団の重心同士を結ぶと負になる——これがSimpsonのパラドックスの正体だ。
どこまでグループ間の差が開くと、パラドックスが起きるのか
上級者の基礎点の優位をΔと呼ぶことにする。主実験のコードでは上級者の基礎点が60.0点・初級者が15.0点だったので、Δ=45点に相当する。このΔを0から70まで動かしたら、プールした相関がどこで符号を変えるのか気になったので、掃引してみた。利用時間の分布とノイズは固定したまま、上級者の基礎点だけを15.0 + Δという形で動かしている。
Δ= 0.00 r_pool= 0.9503 r_A=0.5072 r_B=0.8290
Δ= 10.00 r_pool= 0.9076 r_A=0.5072 r_B=0.8290
Δ= 20.00 r_pool= 0.7880 r_A=0.5072 r_B=0.8290
Δ= 30.00 r_pool= 0.4817 r_A=0.5072 r_B=0.8290
Δ= 40.00 r_pool=-0.0099 r_A=0.5072 r_B=0.8290
Δ= 50.00 r_pool=-0.3953 r_A=0.5072 r_B=0.8290
Δ= 60.00 r_pool=-0.6000 r_A=0.5024 r_B=0.8290
Δ= 70.00 r_pool=-0.7104 r_A=0.4333 r_B=0.8290
グループ内の相関(r_A・r_B)はΔによらずほぼ一定(Δ=60を超えたあたりからr_Aがわずかに下がるのは、スコアの上限100でクリップされる点が出てくるため)なのに対して、プールした相関r_poolはΔが大きくなるにつれて滑らかに1近くから負に落ちていく。ゼロを横切るのはΔ≈39.8点——上級者と初級者の基礎点の差がこれを超えると、プールした相関の符号が反転してパラドックスが発生する境界線だ。

主実験で使ったΔ=45点は、この境界(39.8点)をわずかに超えたところに位置していた。もしΔが39.8点よりわずかに小さい設定を選んでいたら、同じ「グループ内は正の相関」という仕込みのままでも、プールした相関は正のまま(パラドックスなし)になっていたはずだ。パラドックスが起きるかどうかは、グループ内の効果の強さだけでなく、グループ同士がどれだけ離れているかにも左右される、というのが数字として確認できた。
境界の前後で、散布図と回帰直線がどう動くかをアニメーションにした。

Δ=0(上級者と初級者の基礎点が同じ)のときは、2つのクラスタが利用時間軸に沿って自然につながっていて、プールした直線もグループ内の直線とほぼ同じ向きになる。Δを増やしていくと上級者クラスタだけが上に持ち上がっていき、あるところでプール直線が水平を通り越して逆向きに転がり落ちる——この「クラスタの重心が引き離されていく」様子を連続的に見ると、パラドックスが起きる理由が数式より腹落ちする気がした。
意外だったこと
正直、シミュレーションを組む前は「Simpsonのパラドックスなんて、よほど極端な仕込みをしないと起きない特殊ケースだろう」と思っていた。ところが実際にやってみると、Δ=45点というのはそこまで不自然な設定ではない(100点満点のテストで上級者と初級者の基礎点が45点違う、というのは十分あり得る話だ)。しかもグループサイズはn=150ずつと均等、グループ内の効果もどちらも同じ傾き(SLOPE=3.5)で仕込んだ、ある意味かなり”フェア”な設定にもかかわらず、境界のΔ≈39.8点は思ったより低いところにあった。特別な悪意や極端な偏りがなくても、日常的にありそうな部門間・グループ間の差だけで十分パラドックスは起き得る、というのが実測しての実感だった。
まとめ
- 学習アプリの利用時間とテストスコアを、上級者150人(利用時間平均2.94時間・スコア平均70.10)・初級者150人(利用時間平均13.07時間・スコア平均60.44)でシミュレーションした
- グループ内の相関はどちらも強い正(上級者r=0.5508、初級者r=0.8210)。回帰の傾きも3.856・3.584とほぼ同水準
- 区別を外して300人をプールすると、相関はr=-0.2021(p=4.28e-04)まで反転した。個々のグループでは「使うほど伸びる」のに、全体では「使う人ほど低い」という真逆の絵になる
- グループ間の基礎スコア差Δを0〜70点で掃引すると、プールした相関はΔ≈39.8点でちょうど符号を反転させた。グループ内の相関(r_A≈0.51、r_B≈0.83)自体はΔによらずほぼ一定で、パラドックスの有無を決めていたのはグループ間の隔たりの大きさだった
- 極端な仕込みをしなくても(グループサイズ均等、グループ内の効果も同水準)、日常的にありそうな基礎点の差だけでパラドックスは発生し得た
チームやプロダクトの数字を見るとき、「今月の平均利用時間が伸びた」「全体の成約率が上がった」といったトップラインの1つの数字だけで語ってしまうことがよくある。でも今回のシミュレーションが示したのは、その1つの数字の裏で、実は下位グループそれぞれの本当の傾向とは正反対のことが起きていてもおかしくない、ということだった。全体の平均が良くなっていても、実は主力メンバーの生産性が落ちていて、それを新人の頭数増加が覆い隠しているだけかもしれない。逆に全体の数字が悪化して見えても、中身を割ってみればどのグループも改善していて、単に構成比が変わっただけかもしれない。集約された1つの数字を見て安心する前に、それがどんな下部構造の上に乗っているのかを、せめて一度は割って確かめる癖をつけておきたいと思う。
このブログでは、機械学習の仕組みを実際にコードで実装して確かめた実験をテーマ別に整理しています(拡散モデル・Transformer・強化学習・多様体学習・過学習など)。


