統計の土台を「実務のことば」に翻訳する ― 信頼区間・p値・相関≠因果をPythonで確かめる
統計検定3級の知識(平均・分散・確率)を、意思決定に使える「実務のことば」へ翻訳します。月次CVRに信頼区間を付け、施策前後の差が「効果」か「ばらつき」かをPythonで切り分けながら、p値の正しい読み方と「相関≠因果」「予測≠因果」という連載の出発点を確認します。
by Shin
「先月よりCVRが10%上がった」——この差は施策の効果でしょうか、それともただのばらつきでしょうか。
結論から言えば、多くの場合の正解は「いまのデータだけでは区別できない」です。そしてさらに厄介なことに、たとえ差が本物でも、それが施策の"効果"である保証はありません。
この回では、統計検定3級の知識(平均・分散・確率)を「意思決定を変えるための実務のことば」へ翻訳します。信頼区間とp値で"効果かばらつきか"を切り分け、本連載を貫く二つのテーマ——「相関≠因果」「予測≠因果」——の出発点を、過去の受託案件をもとに設計した架空のD2C企業SOLNAのデータとPythonで手を動かして確かめます。
この回の到達目標
- 施策前後の数字の差を、点(2.0%)ではなく 区間([1.8%, 2.2%])で語れるようになる。
- p値と信頼区間が「何を言っていて、何を言っていないか」を誤解なく読めるようになる。
- 「相関≠因果」「予測≠因果」を、自分のデータの例で説明できるようになる。
- これ以降の連載で扱うすべての手法が、この"測り方"の土台の上に乗ることを理解する。
前提
- 第0回(オリエンテーション)を読んでいること。アフォーダンス先行という発想、成熟度のステップ(記述 → 予測 → 因果 → 自動化)、SOLNAの設定を前提にします。
- 統計検定3級相当(平均・分散・割合・初歩の確率・相関の概念)。数式は最小限にとどめ、コードを動かして直感をつくる方針です。
- コードはSOLNAスキーマの
sessionsを想定しますが、本記事は擬似データ生成を含むため、貼り付ければそのまま動きます(numpy/pandas/scipy)。
記述統計から推測統計へ ― 標本と母集団、ばらつきの正体
実務で見ている数字は、ほぼすべて 標本(sample) です。先月のCVR 2.0%は「先月という1回の試行でたまたま観測された値」であって、「SOLNAの真のCVR」そのものではありません。私たちが本当に知りたいのは、観測の背後にある 母集団(population)の真の値 のほうです。
ここで効いてくるのが3級で習った 分散 です。同じ真のCVRからデータを取り直すたびに、観測されるCVRは少しずつ揺れます。この揺れの大きさを定量化したものが 標準誤差(standard error, SE) で、割合の場合は次の形になります。
ポイントは分母の (サンプル数)です。 が大きいほどSEは小さくなり、観測値は真の値の近くに落ち着きます。逆に が小さいと、真の値が変わっていなくても観測値は大きく踊ります。「先月から数字が動いた」の大半は、この踊りを効果と見間違えたものです。
不確実性をどう表すか ― 標準誤差と信頼区間の直感
点推定(2.0%という1つの値)だけを見て意思決定すると、ばらつきを無視することになります。代わりに、真の値が入っていそうな 幅 を添えるのが信頼区間(confidence interval, CI)です。割合の95%CIは、ざっくり「点推定 ± 1.96 × SE」です。
実務での読み替えはこうです。区間が広い=そのKPIはまだ何も断定できない。区間が狭い=動かしてよい。 区間の幅は手元のデータ量が決めるので、CIは「いまの自分のデータで、どこまでの主張が許されるか」の物差しになります。
SOLNAで確かめる ― 月次CVRに区間を付ける
SOLNAのセッションデータで、月次CVR(コンバージョン数 ÷ セッション数)に信頼区間を付けます。割合の区間にはWilson法を使います(端の極端な値でも安定するため、小さな割合の実務ではWaldより堅実です)。
import numpy as np
import pandas as pd
from scipy import stats
rng = np.random.default_rng(42) # 再現用シード
# 月ごとの「真のCVR」(普通は知り得ない)に標本ばらつきを乗せて観測値を作る
true_cvr = {"2025-07": 0.0200, "2025-08": 0.0205, "2025-09": 0.0210,
"2025-10": 0.0208, "2025-11": 0.0235, "2025-12": 0.0240}
N = 20000 # 各月のセッション数
df = pd.DataFrame([
{"month": m, "sessions": N, "conversions": rng.binomial(N, p)}
for m, p in true_cvr.items()
])
df["cvr"] = df["conversions"] / df["sessions"]
# 割合の95%信頼区間(Wilson法)。標準誤差 sqrt(p(1-p)/n) が幅の正体
def wilson(k, n, z=1.96):
p = k / n
d = 1 + z**2 / n
center = (p + z**2 / (2 * n)) / d
half = z * np.sqrt(p * (1 - p) / n + z**2 / (4 * n**2)) / d
return center - half, center + half
df[["lo", "hi"]] = df.apply(
lambda r: pd.Series(wilson(r.conversions, r.sessions)), axis=1)
print(df.assign(cvr=lambda d: (d.cvr * 100).round(2),
lo=lambda d: (d.lo * 100).round(2),
hi=lambda d: (d.hi * 100).round(2)))出力(シード42での例)では、各月のCVRは1.9〜2.4%、95%CIの幅はおおむね ±0.2ポイント(pp)です。たとえば7月は2.0%弱で、区間は約 [1.76%, 2.15%]。つまり「7月の真のCVRは1.8%かもしれないし2.1%かもしれない」というのが、20,000セッションで言える精度の限界です。
この「±0.2pp」という幅を頭に入れたまま、次に進みます。月をまたいだ差がこの幅に収まっているなら、それは効果ではなくばらつきの可能性が高い、という見立てが立てられます。
区間の正体を「実験」で確かめる ― ブートストラップ
SEの式を覚えなくても、区間の意味は実験で体感できます。手元の標本から復元抽出で何度も取り直し、そのたびにCVRを計算してばらつきを見る——これが ブートストラップ です。式の暗記ではなく「同じ母集団から取り直したらどれだけ揺れるか」を直接見る方法で、区間の直感づくりに向いています。
# 同じ母集団から取り直したらどれだけ揺れるかを2000回シミュレートする
july = df.iloc[0]
sample = np.zeros(int(july.sessions))
sample[:int(july.conversions)] = 1 # 1=購入, 0=非購入 のフラグ列
boot = [rng.choice(sample, size=sample.size, replace=True).mean()
for _ in range(2000)]
lo, hi = np.percentile(boot, [2.5, 97.5])
print(f"2025-07 点推定 {july.cvr * 100:.2f}% / "
f"ブートストラップ95%区間 [{lo * 100:.2f}%, {hi * 100:.2f}%]")結果はWilson法とほぼ一致します(例: 約 [1.76%, 2.14%])。式から出した区間も、シミュレーションから出した区間も、語っているのは同じこと——「真の値はこのくらいの幅で揺れる」です。
その差は「効果」か「ばらつき」か ― 検定とp値の実務的な読み方
ここが本回の山場です。2つの月のCVRを比べ、差・差の信頼区間・p値を出します。p値は2標本の比率の検定(正規近似)で計算します。
def compare(a, b):
ra, rb = df.loc[df.month == a].iloc[0], df.loc[df.month == b].iloc[0]
pa, pb = ra.cvr, rb.cvr
diff = pb - pa
se = np.sqrt(pa * (1 - pa) / ra.sessions + pb * (1 - pb) / rb.sessions)
z = diff / se
p = 2 * (1 - stats.norm.cdf(abs(z)))
print(f"{a}({pa*100:.2f}%) -> {b}({pb*100:.2f}%): "
f"差 {diff*100:+.2f}pp(相対 {diff/pa*100:+.1f}%)/ "
f"差の95%CI [{(diff-1.96*se)*100:+.2f}, {(diff+1.96*se)*100:+.2f}]pp / "
f"p={p:.3f}")
compare("2025-07", "2025-08") # 一見プラス成長に見えるが…
compare("2025-07", "2025-11") # こちらは区別できる出力(シード42での例)はこうなります。
| 比較 | 差 | 相対 | 差の95%CI | p値 | 判断 |
|---|---|---|---|---|---|
| 7月 → 8月 | +0.18pp | +9.3% | [−0.10, +0.46]pp | 0.202 | ばらつきと区別できない |
| 7月 → 11月 | +0.32pp | +16.5% | [+0.04, +0.60]pp | 0.026 | ばらつきと区別できる |
注目してほしいのは7月→8月です。「CVRが相対+9.3%」と聞けば社内資料では立派な成果に見えますが、差の信頼区間が0をまたいでいる(マイナスの可能性が残っている)。p値も p = 0.202 で、慣習的なしきい値 p < 0.05 を超えています。つまり「効果があったとは言えない」。一方11月は区間が0をまたがず、ばらつきとは区別できます。
相関は何を語り、何を語らないか ― 疑似相関と交絡の予告
ここまでで「差が本物か(ばらつきでないか)」は判定できるようになりました。ですが——差が本物でも、それが施策の効果とは限りません。 これが連載を貫く「相関≠因果」です。
身近な例で確かめます。「閲覧商品数が多いセッションほどCVRが高い」。これは多くのECサイトで観測される、強くて本物の相関です。ではここから「もっと商品を見せれば買ってくれる」と結論してよいでしょうか。
# 相関≠因果のプレビュー。背後に「購買意欲(intent)」という観測できない交絡がある
n = 30000
intent = rng.beta(1.3, 8, n) # 観測できない購買意欲
product_views = rng.poisson(1 + intent * 12) # 意欲が高い人ほど多く見る
converted = rng.binomial(1, np.clip(0.002 + intent * 0.25, 0, 1)) # 意欲が高い人ほど買う
s = pd.DataFrame({"product_views": product_views, "converted": converted})
s["bucket"] = pd.cut(s.product_views, [-1, 0, 2, 5, 100],
labels=["0", "1-2", "3-5", "6+"])
print(s.groupby("bucket", observed=True)
.agg(sessions=("converted", "size"), cvr=("converted", "mean"))
.assign(cvr=lambda d: (d.cvr * 100).round(2)))出力では、閲覧0商品のCVR約1.7%に対し、6商品以上では約7.7%。きれいな右肩上がりで、相関は文句なしに存在します。しかしこのデータの作り方を見てください。閲覧数も購入も、どちらも intent(購買意欲)という 同じ第三の変数 から生まれています。意欲の高い人が「たくさん見て」「買う」だけで、見ることが買わせているわけではありません。
この intent のように、原因と結果の両方に影響して見せかけの相関を生む変数を 交絡(confounder) と呼びます。もしUI改修で無理に閲覧数を増やしても(意欲の低い人に商品を押し付けても)、CVRは上がりません。「閲覧数を増やす施策」の効果を測りたいなら、意欲という交絡を揃えた比較——つまり因果の道具——が必要になります。これが第4回以降の主題です。
ついでに、この例は 「予測≠因果」 のプレビューにもなっています。product_views はCVRをよく"予測"します(高い人を狙えば当たる)。でも"原因"ではない。予測に効く変数と、介入して効く変数は別物です。第3回ではこの区別をモデルの評価指標とともに詰めます。
回帰の係数を「意味」として読む準備
最後に、次回以降への橋を一本だけ架けておきます。回帰分析の係数は、しばしば「Xが1増えるとYが◯増える」と読まれますが、その読みが因果として正しいのは 交絡が調整されているとき だけです。素朴な回帰は、上の product_views の例と同じ罠に落ちます。「係数の大きさ」より「どの変数を入れて・どれを入れていないか」のほうが、係数の意味を決めます。
この「どの変数を調整すべきか」を、勘ではなく構造で決める道具がDAG(因果ダイアグラム)とバックドア基準です。第4回で扱います。本回でつかんでほしいのは、その手前の感覚——係数を見たら、まず「裏に交絡はいないか」を疑う——という構えです。
フレームとの接続
本回は診断フレームのどの手法にも属さず、すべての手法の前提となる土台 に当たります。成熟度のステップ(記述 → 予測 → 因果 → 自動化)でいえば、最初の「記述」に上がる前の"測り方"そのものです。
ここで身につけた「効果かばらつきか」という問いは、この先ずっと中心に居続けます。第4回のDiD(平行トレンドが本物か)、第7回のCausalImpact(介入後の差が信用区間を超えるか)——いずれも、形を変えた同じ問いです。区間で考える癖は、フレーム全体を回すための共通言語になります。
次回(第2回)は、この土台の上で記述分析に進みます。「媒体が報告するCVと本当の成果の乖離」「コホートの落とし穴」そして「『どこまでデータを遡れるか』を測る接合率(spine coverage)」——適用できる手法の上限を決める概念を導入します。
章末チェックリスト
- 報告された差を見たとき、点推定だけでなく 信頼区間 を確認したか。
- 区間が0(差なし)をまたいでいないか確認したか。またいでいれば「効果あり」とは言わない。
- 「相対+◯%」を見たら、絶対差(pp) と 元の母数 に直して評価したか。
- p値を「効果がない確率」と読んでいないか。「差がない世界での偶然の起こりやすさ」と読めているか。
- 観測された相関について、両方に効く第三の変数(交絡) がいないかを一度疑ったか。
- その変数は「予測に効く」のか「介入して効く」のか、区別して考えたか。
本回は「いま手元のデータで、その差をどこまで信じてよいか」という測り方の土台でした。では自社のデータで、どの手法が実際に成立し、どの意思決定(予算配分・営業優先順位)を動かせるのか——その見立てと判断は、対になる判断レイヤーの記述・予測編で扱います。実装(SQL・コード)は本連載が、成立可否と意思決定への接続は記述・予測編が担う、という二層分業です。