こんにちは。Anagraftの伊藤です。
本コラムは、ベイズ統計を仕事で使えるようになるための入門記事としてまとめました。条件付き確率とベイズの定理から始めて、事前分布と事後分布の考え方、MCMCによる計算、StanやPyMCといった道具の使い方、階層ベイズモデル、そしてベイズ深層学習まで、考え方と使い方を一貫した流れで説明します。
ベイズ統計は、名前を聞く機会のわりに、実務の道具として使っている企業がまだ少ない分野だと感じています。理由はいくつか思い当たります。数式が難しそうに見えること、MCMCという計算の仕組みがブラックボックスに感じられること、そして「普通の統計や機械学習と何が違うのか」が一言で伝わりにくいことです。
本コラムでは、その「何が違うのか」を最初に固めます。ベイズ統計の本質は、答えを1つの数字ではなく分布で持つことにあります。「来月の需要は1,200個」ではなく「1,000個から1,400個の間に95%の確率で収まり、中心は1,200個」と答える。この違いは、外したときの損失が大きい意思決定ほど効いてきます。
また、ベイズ統計はデータが少ない場面に強い、という実務上の大きな利点があります。新製品の立ち上がり、新店舗の売上、めったに起きない故障。データが数件しかない対象でも、事前知識や似た対象の情報を数理的に取り込んで、根拠のある推定を出せます。この仕組みの代表が階層ベイズモデルで、本コラムの中心テーマの一つです。
想定している読者は次のような方です。
前半(第1章から第6章)は考え方と道具立てです。ベイズの定理、事前分布と事後分布、点推定と信用区間、MCMCの仕組み、StanとPyMCの使い方、検定のベイズ的な代替までを順に積み上げます。後半(第7章から第12章)は応用です。階層ベイズ、対戦データからの強さ推定、感染症モデル、状態空間モデル、ベイズ深層学習という実例を通し、最後に「いつベイズを使い、いつ使わないか」という実務判断で締めます。
実例の章では、実際に動かしたコードと結果をそのまま載せています。読み物としてだけでなく、手を動かす際の出発点としても使える構成にしました。
目次
ベイズ統計の歴史は古く、その中核であるベイズの定理は18世紀に生まれています。それが近年あらためて実務の道具として注目されているのは、大きく3つの背景があります。
ベイズ統計は長らく「理屈は美しいが計算できない」統計学でした。答えである事後分布を求めるには、多くの場合、解析的に解けない積分が立ちはだかるからです。この壁を崩したのが、コンピュータの計算力とMCMC(マルコフ連鎖モンテカルロ法)と呼ばれる計算手法の実用化です。いまではノートPCとオープンソースのツールで、20年前なら論文になったようなモデルが数分で推定できます。計算の中身は第4章で、道具は第5章で扱います。
予測や推定の結果を意思決定に直結させる場面が増えるほど、「どのくらい確からしいのか」という情報の価値が上がります。点で出た予測は、当たるか外れるかしか語りません。分布で出た予測は、どこまで外れうるか、最悪の場合に何が起きるかまで語ります。在庫をいくつ積むか、設備をいつ止めるか、価格をどこに置くか。外し方に非対称な損失がある決定では、分布で持つことがそのまま意思決定の質になります。
ビッグデータの時代と言われて久しい一方で、実務の関心事はしばしば「データが少ない対象」に向かいます。新製品、新店舗、新規顧客セグメント、まれにしか起きない故障や事故。深層学習を含む多くの機械学習手法は大量のデータを前提としますが、ベイズ統計は少ないデータと事前知識を組み合わせて、その時点で言える最善の推定を出す枠組みです。データが増えれば推定は自動的に締まっていきます。この性質は第2章と第7章で繰り返し登場します。
本コラムを通じて一貫して伝えたいのは、ベイズ統計が「難解な流派」ではなく「不確実性と付き合うための実用的な道具」だということです。それでは、条件付き確率の復習から始めます。
Anagraftでは、AIプロジェクトの構想・課題設計から、データ分析・機械学習モデルの開発、AI人材の育成まで一貫したご支援を行っています。会社概要・ご支援内容の詳細は、以下の資料からご覧いただけます。
本コラムの構成です。前半が考え方と道具立て、後半が応用と実務判断です。
| 章 | タイトル | 扱う内容 |
|---|---|---|
| 第1章 | 条件付き確率とベイズの定理、考え方の転換 | ベイズの定理、頻度論との違い、信念の更新という見方 |
| 第2章 | 事前分布・尤度・事後分布、ベイズ更新の仕組み | 3点セットの役割、共役事前分布、事前分布の選び方 |
| 第3章 | ベイズ推定の実際、点推定と信用区間 | MAP推定と事後平均、信用区間、予測分布、損失関数 |
| 第4章 | なぜMCMCが必要か、その仕組み | メトロポリス法からNUTSまで、収束の考え方 |
| 第5章 | StanとPyMCで始めるベイズモデリング | ツールの選び方、標準ワークフロー、収束診断の実務 |
| 第6章 | 検定のベイズ的代替、A/BテストをP(A>B)で答える | p値の限界、事後確率での意思決定、早期停止 |
| 第7章 | 階層ベイズモデル、少ないデータから借りる力 | 部分プーリング、縮約、地域別リスク推定の実例 |
| 第8章 | 実例で学ぶ、対戦データから強さを推定する | プロテニスの対戦データによる潜在的な強さの推定 |
| 第9章 | 実例で学ぶ、感染症の収束をSIRモデルとベイズで予測する | 構造モデルとベイズ推定の組み合わせ、幅を持った予測 |
| 第10章 | 状態空間モデル、時系列をベイズで読む | 観測と状態の2階建て、トレンドと季節性の分解 |
| 第11章 | ベイズ深層学習、予測の自信を測る | MC Dropout、予測エントロピー、不確実性の実務活用 |
| 第12章 | 実務適用の設計、ベイズをいつ使い、いつ使わないか | 採否の判断基準、導入の進め方、運用と報告様式 |
ベイズ統計を学ぶときに最初につまずきやすいのは、数式そのものではありません。確率という言葉が指しているものが、学校で習ったときとは少し違う、という点です。ベイズの枠組みでは、確率は「何度も繰り返したときにどれくらいの割合で起きるか」だけを指すのではなく、「いま手元にある情報のもとで、その主張をどの程度もっともらしいと考えているか」を表す数値としても扱われます。この一点が飲み込めると、あとに続く事前分布・尤度・MCMC・階層モデルといった話は、すべて同じ考え方の延長として読めるようになります。
この章では、条件付き確率という土台から出発して、ベイズの定理を導き、その各項が何を意味しているのかを整理します。あわせて、医療検査の陽性的中率という定番の例を数値つきでたどり、前提となる確率が変わるだけで結論がどれほど動くかを確認します。最後に、この考え方が経営やDXの意思決定と相性が良い理由を述べます。事前分布の選び方や更新の繰り返しについては第2章、推定値と区間の読み方は第3章で扱いますので、ここでは考え方の骨格に絞ります。
条件付き確率は、「ある事実がわかっている状況に限ったときの確率」です。事象\(A\)が起きているとわかっているときに事象\(B\)が起きる確率を\(P(B \mid A)\)と書き、次のように定義します。
\( P(B \mid A) = \dfrac{P(A \cap B)}{P(A)} \)
分母の\(P(A)\)は、\(A\)が起きた世界だけに視野を絞るという操作にあたります。分子の\(P(A \cap B)\)は、その絞った世界のなかで\(B\)も同時に起きている部分です。全体の面積で割るのではなく、条件で切り取った面積で割る、という違いだけです。定義としてはこれだけのものですが、実務でこの計算を誤ると結論が大きくずれます。
もっとも多い誤りは、\(P(B \mid A)\)と\(P(A \mid B)\)を同じものとして扱ってしまうことです。この二つはまったく別の量です。たとえば「不良品であったときに検査で異常と判定される確率」と、「検査で異常と判定されたときに実際に不良品である確率」は、字面が似ているだけで数値はまるで違います。前者は検査装置の性能を表す数値であり、後者は現場が知りたい数値です。両者をつなぐ橋がベイズの定理です。
定義の式を移項すると、\( P(A \cap B) = P(B \mid A)\,P(A) \) となります。同じ量を逆向きに分解すれば \( P(A \cap B) = P(A \mid B)\,P(B) \) とも書けます。この二つが等しいという、それだけの事実からベイズの定理が出てきます。
\( P(A \mid B) = \dfrac{P(B \mid A)\,P(A)}{P(B)} \)
導出そのものは中学校の分数計算と変わりません。にもかかわらず、この式が統計学の大きな流れを作ってきたのは、式の解釈のしかたに幅があるからです。
確率の意味の取り方には、大きく二つの立場があります。
ひとつは頻度としての解釈です。同じ条件で試行を無限に繰り返したとき、その事象が起きる割合の極限を確率と呼びます。サイコロの1が出る確率が\(1/6\)である、というときの使い方です。この立場では、確率は繰り返し可能な現象についてしか定義できません。したがって「この新製品が来期に黒字化する確率」は、繰り返しようがないので厳密には確率の対象になりません。また、真の不良率や真の平均といった母数(データの背後にある本当の値)は、たまたま揺らぐものではなく決まった一つの値だと考えるため、母数そのものに確率を割り当てることもしません。統計的検定や信頼区間は、この立場のうえに組み立てられています。
もうひとつは、信念の度合いとしての解釈です。ここでは確率を、ある主張に対して自分がどの程度の確からしさを与えているかの数値として扱います。0は完全に否定、1は完全に肯定、0.7なら「かなりありそうだが確信はしていない」という状態を表します。この立場をとると、繰り返せない一回限りの出来事についても確率が言えますし、母数そのものについて「真の不良率は0.3%から0.8%のあたりにありそうだ」と分布の形で語ることができます。ベイズ統計はこちらの立場に立ちます。
信念の度合いというと主観的で恣意的に聞こえますが、勝手に決めてよいという話ではありません。信念の度合いは、確率の基本的な性質(合計が1になる、排反な事象の確率は足せる、など)を満たすように整合的でなければならず、いったん整合的に置いた数値は、新しい情報が入るたびにベイズの定理という決まった手続きで更新されます。出発点に判断の余地があるだけで、更新のルール自体は機械的です。同じデータを与えれば、同じ出発点からは必ず同じ結論に着きます。
また、データが増えるにつれて出発点の影響は薄れていきます。少数の観測しかないうちは出発点の置き方で結論が変わりますが、これは欠点というより、情報が足りない状態を正直に表しているだけです。むしろ、データが少ないときに出発点を明示しないまま推定値だけを出す方が、判断を誤らせます。
| 観点 | 頻度論の立場 | ベイズの立場 |
|---|---|---|
| 確率とは | 繰り返したときに起きる割合 | その主張をどの程度確からしいと考えているか |
| 母数の扱い | 決まった一つの値。確率は与えない | 分布として表す。幅そのものが答えの一部 |
| 一回限りの出来事 | 原則として扱わない | 扱える |
| 過去の知見 | 手続きの外に置く | 出発点として式の中に入れる |
| 出てくる答え | 検定の結果と信頼区間 | 更新後の分布。そこから確率で答えられる |
この表を、どちらが正しいかの対比として読む必要はありません。問いの立て方が違うだけです。品質管理の工程能力のように、同じ条件の繰り返しが実際に存在する場面では頻度論の道具立てが素直に効きます。一方、初回の投資判断のように繰り返しが存在しない場面では、頻度としての確率は定義できず、ベイズの立場のほうが問いに答えられます。
考え方の違いがもっともはっきり出るのが、検査の解釈です。次の設定で考えます。数値は説明のための仮のものです。
この検査で陽性と出た人が、実際に病気である確率はどれくらいでしょうか。感度も特異度も99%なので9割以上だろう、という感覚を持つ方が多いのですが、実際には大きく外れます。
10万人が受診したとして数えます。有病率0.1%なので、病気の人は100人、健康な人は99,900人です。病気の100人のうち感度99%で陽性になるのは99人。健康な99,900人のうち1%が誤って陽性になるので999人。陽性者は合わせて1,098人です。このうち本当に病気なのは99人ですから、
\( P(\text{病気} \mid \text{陽性}) = \dfrac{99}{99 + 999} = \dfrac{99}{1098} \approx 0.090 \)
およそ9.0%です。陽性と言われても、実際に病気である確率は1割に届きません。理由は単純で、母集団のうち健康な人が圧倒的に多いため、1%という小さな誤陽性率でも人数に直すと999人になり、真の陽性99人を10倍も上回ってしまうからです。検査の精度が悪いのではなく、探している対象がもともと稀であることが効いています。

同じ計算を陰性側でも行うと、対照的な結果になります。陰性になるのは、病気なのに見逃された1人と、健康で正しく陰性になった98,901人です。陰性と出たときに実際に健康である確率は99.99%を超えます。この検査は「陰性なら安心してよい」という判断には強く効き、「陽性なら病気だ」という判断には弱い、という非対称な性質を持っています。ひとつの検査でも、どちら向きに使うかで信頼度がまるで違うということです。
いまの計算で決定的な役割を果たしたのは、有病率0.1%という数値でした。これがベイズの枠組みでいう事前確率、すなわちデータを見る前の段階でその主張に与えている確からしさです。事前確率を変えて同じ計算を繰り返すと、陽性的中率は次のように動きます。感度99%・特異度99%は固定しています。
| 事前確率(有病率) | 陽性のうち本当に病気である割合 | 読み方 |
|---|---|---|
| 0.01% | 約0.98% | 陽性でもほぼ誤検出 |
| 0.1% | 約9.0% | 陽性でも1割に届かない |
| 1% | 50.0% | ちょうど五分五分 |
| 5% | 約83.9% | 陽性はかなり信用できる |
| 20% | 約96.1% | ほぼ確定に近い |
| 50% | 99.0% | 感度の数値とほぼ一致 |
検査そのものはまったく変えていないのに、答えは0.98%から99.0%まで動きます。同じ「陽性」という結果が、どんな集団に対して行われたかによって、意味がまるで変わるということです。集団検診として無選別に実施した場合と、症状があって受診した人に実施した場合とでは、同じ陽性でも解釈が違って当然です。医療の現場で検査前確率が重視されるのは、このためです。
この構造は医療に限りません。不正検知、故障予兆、解約予測、セキュリティのアラートなど、探している対象がもともと少ない場面ではすべて同じことが起きます。精度の数値だけを見て導入を決めると、運用が始まってから「アラートのほとんどが空振りだ」という事態に直面します。モデルの性能指標と、現場が受け取る通知の意味は別物であり、両者をつなぐのが事前確率です。
もう一点、実務で有用なのは、検査を重ねたときの扱いです。1回目が陽性だった人は、その時点で病気である確率が9.0%になっています。2回目の検査を行うときは、この9.0%を新しい出発点として同じ計算を繰り返します。誤りが独立に起きると仮定すれば、2回連続で陽性だった場合の確率はおよそ90.8%になります。1回目の陽性で9.0%、2回目でも陽性なら90.8%です。証拠を一つずつ積み上げて確からしさを更新していくという操作が、この一連の計算でそのまま表現されています。
ここまでは事象の確率として書いてきましたが、統計の文脈では、確かめたい仮説や知りたい母数を\(\theta\)、観測されたデータを\(D\)と置いて次のように書きます。
\( P(\theta \mid D) = \dfrac{P(D \mid \theta)\,P(\theta)}{P(D)} \)
四つの項には、それぞれ役割があります。
事前確率 \(P(\theta)\) は、データを見る前の段階での確からしさです。過去の実績、業界の相場、専門家の見解、物理的な制約などが根拠になります。検査の例では有病率0.1%がこれにあたります。何の手がかりもない場合は、広く薄く分布させて「よくわからない」という状態を表現します。事前確率をどう置くかは第2章で詳しく扱います。
尤度 \(P(D \mid \theta)\) は、仮説\(\theta\)が正しいと仮定したときに、いま観測されたデータが得られる確からしさです。日本語では「ゆうど」と読み、もっともらしさを意味します。検査の例では、病気の人が陽性になる確率99%と、健康な人が陽性になる確率1%が尤度にあたります。注意したいのは、尤度は\(\theta\)についての確率ではないという点です。データを固定して\(\theta\)を動かしたときの値であり、\(\theta\)について足し合わせても1にはなりません。あくまで、どの仮説がいまのデータをよく説明するかを比べるための物差しです。
周辺尤度 \(P(D)\) は、考えられるすべての仮説を通して、そのデータが観測される確からしさです。エビデンスとも呼ばれます。仮説が有限個なら
\( P(D) = \sum_i P(D \mid \theta_i)\,P(\theta_i) \)
連続的な母数なら
\( P(D) = \int P(D \mid \theta)\,P(\theta)\,d\theta \)
と書きます。検査の例では、陽性者1,098人(病気の99人と健康な999人の合計)を10万人で割った値がこれにあたります。役割としては、分子の値を合計1になるように調整する規格化の定数です。\(\theta\)を含まないので、どの仮説がもっともらしいかという順位づけには影響しません。ただし、この積分がほとんどの実問題で解析的に計算できないことが、ベイズ統計の長年の実務上の壁でした。第4章で扱うMCMCは、この壁を計算の工夫で迂回する方法です。
事後確率 \(P(\theta \mid D)\) は、データを見た後の確からしさです。検査の例では9.0%がこれにあたります。ここで重要なのは、事後確率が次の判断の事前確率になる、という点です。2回目の検査の計算で9.0%を出発点にしたのは、まさにこの性質によります。情報が入るたびに出発点が書き換わり、次の情報を待つ。この繰り返しがベイズ更新です。
四つの項の関係は、次のようにまとめられます。
\( \text{事後確率} \propto \text{尤度} \times \text{事前確率} \)
記号\(\propto\)は「比例する」という意味で、周辺尤度による割り算を省いた形です。分母は\(\theta\)によらない定数なので、仮説どうしを比べるだけならこの形で十分です。ベイズの定理の実質は、事前の見立てに、データがもたらした証拠の強さを掛け合わせて、見立てを更新する、という一行に尽きます。

実務で暗算に近い感覚をつかむには、確率ではなくオッズで考えると便利です。オッズは、ある主張が正しい確率と正しくない確率の比です。確率0.2ならオッズは\(0.2/0.8 = 0.25\)、確率0.5ならオッズは1になります。
ベイズの定理を、仮説\(H_1\)と対立する仮説\(H_0\)の比の形で書くと、周辺尤度が分子分母で打ち消し合って次のようになります。
\( \dfrac{P(H_1 \mid D)}{P(H_0 \mid D)} = \dfrac{P(D \mid H_1)}{P(D \mid H_0)} \times \dfrac{P(H_1)}{P(H_0)} \)
右辺の最初の項は尤度比と呼ばれ、そのデータがどちらの仮説をどれだけ支持するかの倍率を表します。式全体は「事後オッズ=尤度比×事前オッズ」という形で、更新が単なる掛け算になります。計算が難しい周辺尤度が消えるので、手元でも扱えます。
検査の例で確かめます。事前オッズは\(0.001/0.999 \approx 0.001\)。陽性という結果の尤度比は、感度99%を誤陽性率1%で割って99倍です。事後オッズは\(0.001 \times 99 \approx 0.099\)。これを確率に直すと\(0.099/(1+0.099) \approx 0.090\)となり、先ほどの9.0%と一致します。
この形にすると、証拠の強さを倍率として持ち歩けるようになります。尤度比が99倍というのは相当に強い証拠ですが、事前オッズが\(1/999\)と極端に小さいため、99倍しても五分五分には届かない、という構図が見て取れます。
営業の現場に置き換えてみます。ある案件の受注確率を、これまでの実績から20%と見積もっているとします。事前オッズは0.25です。ここで、初回商談に決裁者が同席したという情報が入りました。過去のデータで、受注した案件の60%は決裁者が同席していた一方、失注した案件では20%だったとすると、尤度比は3倍です。事後オッズは0.75、確率に直すと約42.9%になります。さらに予算が確保済みだという情報が入り、その尤度比が2倍だとすれば、オッズは1.5、確率は60%です。数値は仮のものですが、判断材料が増えるたびに見立てを何倍かに動かしていく、という更新のしかたが具体的につかめると思います。
以下は、この計算をそのままコードにしたものです。数値を差し替えて手元で確認できます。
def to_odds(p):
"""確率をオッズに変換する"""
return p / (1 - p)
def to_prob(odds):
"""オッズを確率に戻す"""
return odds / (1 + odds)
def update(prior_prob, likelihood_ratio):
"""事後オッズ = 尤度比 x 事前オッズ"""
return to_prob(to_odds(prior_prob) * likelihood_ratio)
# 検査の例: 有病率0.1%, 感度99%, 特異度99%
prevalence = 0.001
sensitivity = 0.99
specificity = 0.99
lr_positive = sensitivity / (1 - specificity) # 陽性という結果の尤度比
posterior = update(prevalence, lr_positive)
# 2回目も陽性だった場合は、1回目の事後確率を出発点にする
posterior_2nd = update(posterior, lr_positive)
# 証拠を順に掛けていく例(受注確率の更新)
p = 0.20
for lr in [3.0, 2.0]:
p = update(p, lr)
検査の例と同じ構造は、AIを使った検知の仕組みでそのまま現れます。製造ラインの外観検査を例にします。数値は説明のための仮のものです。
1万個を流したとすると、不良品は50個、良品は9,950個です。検出される不良品は\(50 \times 0.95 = 47.5\)個、誤って検出される良品は\(9950 \times 0.02 = 199\)個。アラートは合わせて246.5件で、そのうち本当に不良なのは47.5件ですから、的中率は約19.3%です。アラートの8割は空振りということになります。
この数値をモデルの失敗と読むかどうかは、目的によります。50個の不良のうち47.5個を捕まえられているので、流出防止という目的には十分に働いています。一方、検査員の工数という観点では、246.5件を人が見る必要があるため、負荷は小さくありません。導入判断で見るべきなのは検出率だけでも的中率だけでもなく、両者と、見逃し1件のコスト、空振り1件のコストを並べた損得です。
ここで効くのは、やはり事前確率です。不良率が0.5%ではなく5%の工程であれば、同じモデルでも的中率は約71.4%まで上がります。逆に不良率0.05%の工程では約2.3%まで落ちます。同じモデルを別の工程に横展開したときに現場の実感が変わるのは、モデルが劣化したからではなく、対象となる母集団の構成が変わったからです。ベイズの考え方を持っていると、この違いを「事前確率が変わった」という一語で説明できます。
もうひとつ、アラートを人が見る運用では、判断がベイズ更新の形をとっていることにも触れておきます。検査員はアラートを受け取った時点で19.3%の見立てを持ち、対象を目視した結果という新しい証拠でその見立てを更新しています。人とモデルの役割分担を設計するときは、モデルが出すのは最終判断ではなく事前確率の更新である、と位置づけると整理しやすくなります。
経営やDXの現場での判断は、多くの場合、次のような形をしています。手元には限られた情報しかなく、それでも期限までに決めなければならない。決めた後で新しい情報が入り、続きの判断はその情報を織り込んで行う。この繰り返しが実務です。
この形は、ベイズ更新の構造とそのまま重なります。判断の出発点になる見立てが事前確率、入ってきた情報が尤度、更新後の見立てが事後確率です。そして事後確率が次の判断の事前確率になります。特別な考え方を新しく導入するというより、すでに行っている判断の流れに、数値と手続きを与えるものだと捉えると近いと思います。
この枠組みが実務にもたらす利点を、四つ挙げます。
一方で、注意すべき点もあります。事前確率の置き方には判断が入るため、置き方の根拠を残しておかないと、後から結論の妥当性を検証できません。恣意的に事前確率を選べば結論を動かせてしまうという批判は、実務では現実の懸念です。対処としては、事前分布の根拠を文書に残すこと、複数の置き方で結論がどう変わるかを確かめること(感度分析)、データが十分にあるかどうかを確認することが基本になります。この点は第2章と第12章で改めて扱います。
歴史的には二つの立場のあいだに論争がありましたが、実務では対立として捉える必要はありません。問いの形に応じて道具を選べば十分です。
| 状況 | 向いている考え方 | 理由 |
|---|---|---|
| 大量のデータがあり、単純な比較をしたい | 頻度論の手法で足りることが多い | データが十分なら結論はほぼ一致し、計算も軽い |
| データが少なく、過去の知見がある | ベイズ | 事前知識を明示的に組み込める |
| 繰り返しのない一回限りの判断 | ベイズ | 頻度としての確率が定義できない |
| データが逐次的に増えていく | ベイズ | 更新の手続きがそのまま使える |
| 店舗や地域ごとにデータ量が偏っている | ベイズ(階層モデル) | データの少ない単位が全体の情報を借りられる。第7章で扱う |
| 予測の確からしさ自体を出したい | ベイズ | 予測が分布として得られる。第11章で扱う |
| 規制や業界慣行で手法が決まっている | 指定された手法 | 結果を受け取る側の合意が優先される |
実際、データが豊富にある状況では、両者の結論はほとんど一致します。事前確率の影響が薄れ、尤度だけで結論が決まるためです。違いが表面化するのは、データが少ないとき、判断が一回限りのとき、そして「確率でいくつか」という形の答えが求められるときです。ベイズの手法を使うかどうかは、その場面に当たっているかどうかで決めれば足ります。
この章で押さえておきたい点を整理します。
次の第2章では、事前分布・尤度・事後分布の関係をより具体的に扱い、更新が繰り返されるとどのように見立てが定まっていくのかを見ていきます。この章で確率の意味の取り方が変わった感覚をつかんでおけば、そこから先は同じ考え方の積み重ねになります。
『完全独習 ベイズ統計学入門』(小島寛之、ダイヤモンド社):面積図を使ってベイズの定理を組み立てる構成で、数式に慣れていない方でも条件付き確率から事後確率までを追えます。この章で扱った検査の例のような、直感が外れる場面の理解に向いています。
『入門 ベイズ統計 意思決定の理論と発展』(松原望、東京図書):ベイズの考え方を意思決定の枠組みとして位置づけて解説しており、確率を信念の度合いとして扱う立場の背景を丁寧にたどれます。ビジネスの判断にどうつなげるかを考えるうえで参考になります。
ベイズ統計の計算は、突き詰めれば三つの部品でできています。推定したい量について、データを見る前に持っている見込みを表す事前分布。手元のデータが、推定したい量の値ごとにどれくらい起こりやすいかを表す尤度。そして、その二つを組み合わせた結果として得られる事後分布です。この三つの関係を式で書くと次のようになります。
\( p(\theta \mid D) \propto p(D \mid \theta) \, p(\theta) \)
\( \theta \) は推定したい量です。コンバージョン率、機械の故障率、施策の効果量など、値が分からないまま意思決定の前提になっている数値がこれにあたります。\( D \) は観測されたデータです。\( p(\theta) \) が事前分布、\( p(D \mid \theta) \) が尤度、\( p(\theta \mid D) \) が事後分布です。記号 \( \propto \) は「比例する」という意味で、右辺を全体で割り算して面積を1に揃えれば左辺になる、という関係を表します。
言葉にすれば、事後分布は事前分布と尤度の掛け算です。データを見る前の見込みに、データが持ち込んだ情報を掛け合わせて、見込みを更新する。ベイズ統計でやっていることは、細かい技法を取り払えばこれだけです。第1章で見たベイズの定理を、点の確率ではなく分布の更新として書き直したものが上の式にあたります。
三つの部品それぞれが実務で持つ意味は、はっきり分かれています。事前分布は、過去の実績や業界の相場観といった、データセットの外側にある知識を計算に持ち込む入口です。尤度は、データがどういう仕組みで生まれたと考えるか、つまりモデルの仮定そのものです。事後分布は、その二つを踏まえた結論であり、意思決定に使う唯一の出力です。分析の結果が期待と違ったとき、どこを疑えばよいかを切り分けるためにも、この三分割は覚えておく価値があります。
尤度は、慣れるまで扱いにくい概念です。\( p(D \mid \theta) \) という式は確率の形をしていますが、ベイズ推定の文脈では \( D \) の側が観測済みで固定され、\( \theta \) の側を動かして値を見ます。つまり「データを固定したうえで、パラメータの値ごとに、そのデータの起こりやすさを返す関数」として使います。この向きで読むとき、これは \( \theta \) についての確率分布ではありません。\( \theta \) を全域で積分しても1にはならないのが普通です。
具体例で考えます。あるランディングページに200人が訪れ、そのうち8人が申し込んだとします。1人ずつが独立に確率 \( \theta \) で申し込むと考えれば、このデータの起こりやすさは二項分布で書けます。
\( p(D \mid \theta) = \binom{200}{8} \theta^{8} (1-\theta)^{192} \)
この式に \( \theta = 0.01 \) を入れると小さな値が返り、\( \theta = 0.04 \) を入れると大きな値が返り、\( \theta = 0.20 \) を入れるとまた小さな値が返ります。最大になるのは \( \theta = 8/200 = 0.04 \) のときで、これが最尤推定値です。頻度論の推定は、ここで止まって「4.0%」という一点を答えとして返します。ベイズ推定は、この関数を事前分布と掛け合わせる材料として使います。
実務上、尤度を決めるという作業は「データがどう生まれたか」の想定を書き下すことに等しく、分析の当たり外れの大部分がここで決まります。訪問者ごとの申し込み確率が本当に共通なのか、流入チャネルによって違うのではないか、同じ人が複数回カウントされていないか。二項分布を選んだ時点でこうした仮定を置いたことになります。この仮定が現実とずれていれば、事前分布をどう工夫しても結論はずれます。事前分布の選び方の議論は目立ちますが、尤度の設定のほうが結果への影響は大きいのが通例です。
先ほどの式で \( \propto \) を使ったのは、割り算の分母を省略したからです。省略せずに書くと次のようになります。
\( p(\theta \mid D) = \dfrac{p(D \mid \theta) \, p(\theta)}{\displaystyle\int p(D \mid \theta) \, p(\theta) \, d\theta} \)
分母は 周辺尤度 と呼ばれます。\( \theta \) が取りうるあらゆる値について、事前分布の重みをかけながら尤度を足し合わせた量で、モデル全体から見たときにそのデータがどれくらい出やすいかを表します。分母には \( \theta \) が残らないため、事後分布の形そのものは分子だけで決まります。分母は、分子が描く曲線の面積を1に揃えるための定数として働きます。
この分母が、ベイズ統計の実装上の難所です。パラメータが1個なら1次元の積分で済みますが、回帰係数が20個あるモデルなら20次元の積分になり、解析的に解ける形はほとんどありません。この積分を避けて事後分布を扱う方法として編み出されたのがマルコフ連鎖モンテカルロ法であり、その仕組みは第4章で扱います。本章では、この積分が手計算で解けてしまう特別な組み合わせに絞って、更新の中身を見ていきます。
掛け算という構造から分かることがいくつかあります。第一に、事前分布がゼロを与えた領域は、尤度がどれだけ大きくても事後分布でゼロのままです。「この値はありえない」と事前に決め打ちすると、データがいくらそれを支持しても結論は動きません。事前分布で範囲を切るときは、この不可逆性を意識する必要があります。第二に、事前分布と尤度が指す方向が大きく食い違うと、事後分布は両者の間のどこかに落ち着きます。落ち着く位置は、それぞれがどれだけ尖っているか、つまりどれだけ強い情報を持っているかで決まります。

ベイズ更新には、実務で効いてくる性質があります。データを一度にまとめて処理しても、小分けにして順番に処理しても、同じ事後分布にたどり着くという性質です。
データを \( D_1 \) と \( D_2 \) に分けて考えます。まず \( D_1 \) で更新すると、事後分布は \( p(\theta \mid D_1) \propto p(D_1 \mid \theta) p(\theta) \) になります。次に、これを事前分布として \( D_2 \) で更新すると、\( p(\theta \mid D_1, D_2) \propto p(D_2 \mid \theta) p(\theta \mid D_1) \propto p(D_2 \mid \theta) p(D_1 \mid \theta) p(\theta) \) となります。最後の形は、\( D_1 \) と \( D_2 \) をまとめて一度に処理した場合と同じです。
つまり、昨日の分析結果である事後分布を、今日の分析の事前分布としてそのまま使えます。データが日々増えていく運用では、この性質がそのまま実装の設計になります。過去の全データを毎回読み直す必要はなく、前回の事後分布と今回の新規データだけを持てば更新できます。データ量が増えても更新のコストが増えない構造をとれるということです。
この逐次性は、ダッシュボードや監視の仕組みと相性がよい性質です。日次でコンバージョン率を追う、週次で故障率を見直す、四半期ごとに需要予測の前提を更新するといった運用では、「最新の結論」を状態として保持し続けることになります。頻度論の枠組みで同じことをしようとすると、検定を繰り返すたびに有意水準の扱いが問題になりますが、ベイズ更新では分布をそのまま持ち越すだけで整合します。
ただし、この性質が成り立つのは、データが同じ仕組みから生まれ続けている場合に限られます。途中でサイト構造を変えた、広告の出稿先を変えた、季節要因が入った、といった変化があれば、前回の事後分布をそのまま今回の事前分布にするのは適切ではありません。この場合は、事後分布をわざと広げてから使う(過去の情報を割り引く)、あるいは変化点を明示的にモデルに入れるといった対処が要ります。時間とともに変わる量をどう扱うかは第10章の状態空間モデルの主題です。
先ほど触れた分母の積分は、一般には解けません。ところが、尤度の形に合わせて事前分布の形をうまく選ぶと、事後分布が事前分布と同じ種類の分布になり、積分を実際に計算しなくても答えが分かる場合があります。この関係にある事前分布を 共役事前分布 と呼びます。
共役という言葉は難しく聞こえますが、やっていることは単純です。事前分布と尤度を掛けたときに、\( \theta \) の指数部分が足し算になるだけで、関数の骨格が変わらないような組み合わせを選ぶ、というだけです。骨格が変わらなければ、パラメータの数値を足し算するだけで更新が終わります。
代表的な組み合わせを整理します。
| データの種類 | 尤度(データの分布) | 共役な事前分布 | 実務での典型例 |
|---|---|---|---|
| 成功か失敗かの2値 | 二項分布・ベルヌーイ分布 | ベータ分布 | コンバージョン率、不良率、解約率 |
| 件数(回数) | ポアソン分布 | ガンマ分布 | 問い合わせ件数、故障発生件数 |
| 連続値(分散が既知) | 正規分布 | 正規分布 | 測定値の平均、リードタイム |
| 3種類以上の選択肢 | 多項分布 | ディリクレ分布 | 選択されたプランの構成比 |
| 発生までの時間 | 指数分布 | ガンマ分布 | 設備の寿命、待ち時間 |
共役の組み合わせは計算が軽く、更新が足し算で済むため、仕組みを理解するうえでも実装の初手としても便利です。一方で、現実のモデルが常に共役の枠に収まるわけではありません。説明変数を入れた回帰、階層構造を持つモデル、複数の効果が絡むモデルでは、共役性は失われます。そこでマルコフ連鎖モンテカルロ法が必要になるわけですが、共役の場合に更新がどう進むかを一度手で追っておくと、後の章で計算機がやっていることの見当がつきやすくなります。
もっとも使う機会が多い、二値データとベータ分布の組み合わせを具体的に追います。ベータ分布は、0から1の間の値をとる量、つまり比率や確率そのものを表すのに適した分布です。二つの正の数 \( a \) と \( b \) で形が決まり、密度は次の形をしています。
\( p(\theta) = \dfrac{1}{B(a,b)} \theta^{a-1} (1-\theta)^{b-1} \)
\( B(a,b) \) は面積を1に揃えるための定数です。この分布の平均は \( a/(a+b) \) で、\( a + b \) が大きいほど分布は細く尖ります。直感的には、\( a \) が成功の回数、\( b \) が失敗の回数に対応し、\( a+b \) が「事前にどれくらいの観測を経験したことにするか」を表す量になります。
ここに、\( n \) 回の試行で \( s \) 回成功したというデータが入ります。尤度は定数部分を除けば \( \theta^{s} (1-\theta)^{n-s} \) です。事前分布と掛け合わせます。
\( p(\theta \mid D) \propto \theta^{s} (1-\theta)^{n-s} \times \theta^{a-1} (1-\theta)^{b-1} = \theta^{a+s-1} (1-\theta)^{b+n-s-1} \)
右端の形を見ると、これはベータ分布の密度そのもので、パラメータが \( a+s \) と \( b+n-s \) になっています。つまり更新則は次の一行に尽きます。
\( \mathrm{Beta}(a, b) \;\xrightarrow{\;n \text{回中} s \text{回成功}\;}\; \mathrm{Beta}(a+s,\; b+n-s) \)
成功した数を \( a \) に足し、失敗した数を \( b \) に足す。これだけです。積分は一度も計算していません。参考までに、省略した分母(周辺尤度)も閉じた形で書けて、\( p(D) = \binom{n}{s} B(a+s, b+n-s) / B(a,b) \) となります。事後分布の形を知るだけならこの値は不要ですが、モデル同士を比較する場面では使うことになります。
数字を入れて動かします。あるサービスの申し込みページのコンバージョン率を推定する場面を考えます。同種のページの実績から、だいたい3%前後だろうという見込みがあるとします。この見込みを \( \mathrm{Beta}(3, 97) \) で表します。平均は \( 3/100 = 0.03 \) で、\( a+b = 100 \) ですから、「過去に100人が訪れて3人が申し込んだ程度の情報を持っている」という強さの主張になります。標準偏差はおよそ1.7ポイントで、2%台から5%あたりまでを広めに見込んでいる形です。
ここに、新しいページで200人が訪問し8人が申し込んだというデータが入ります。更新則を当てはめます。
\( \mathrm{Beta}(3, 97) \;\rightarrow\; \mathrm{Beta}(3+8,\; 97+192) = \mathrm{Beta}(11, 289) \)
事後分布の平均は \( 11/300 = 0.0367 \) で、3.67%です。データだけを見た比率は \( 8/200 = 4.0\% \)、事前の見込みは3.0%でしたから、結論はその間に落ち着きました。落ち着く位置は偶然ではなく、次の形に必ず分解できます。
\( E[\theta \mid D] = \dfrac{a+s}{a+b+n} = \dfrac{a+b}{a+b+n} \cdot \dfrac{a}{a+b} \;+\; \dfrac{n}{a+b+n} \cdot \dfrac{s}{n} \)
事後分布の平均は、事前分布の平均とデータから計算した比率の重み付き平均です。重みは、それぞれが背負っている観測数の比になります。今回は事前が100、データが200ですから、重みは3分の1と3分の2です。実際に \( (1/3) \times 3.0\% + (2/3) \times 4.0\% = 3.67\% \) となり、先ほどの値と一致します。ベイズ更新は、直感的には「事前の経験とデータを、それぞれの情報量に応じて按分する」操作だと理解できます。
逐次更新でも結果が変わらないことを確かめます。200人を2日に分けて、初日は100人中3人、翌日は100人中5人が申し込んだとします。初日の更新で \( \mathrm{Beta}(3+3, 97+97) = \mathrm{Beta}(6, 194) \)。これを事前分布として翌日を処理すると \( \mathrm{Beta}(6+5, 194+95) = \mathrm{Beta}(11, 289) \)。まとめて処理した場合と一致します。
Pythonで同じ計算を書くと次のようになります。更新則が足し算であるため、確率分布のライブラリを呼ばなくても事後分布のパラメータは求まります。分布の形を描いたり要約量を出したりする部分だけをライブラリに任せる形です。
import numpy as np
from scipy import stats
def update_beta(a, b, n, s):
"""ベータ事前分布を、n回中s回成功のデータで更新する"""
return a + s, b + n - s
# データを見る前の見込み: 平均3%、観測100件相当の強さ
a0, b0 = 3.0, 97.0
# 1) まとめて処理する
a_all, b_all = update_beta(a0, b0, n=200, s=8)
# 2) 日ごとに逐次で処理する
a, b = a0, b0
daily = [(100, 3), (100, 5)] # (訪問数, 申込数)
for n, s in daily:
a, b = update_beta(a, b, n, s)
# 1) と 2) は同じパラメータになる
assert (a, b) == (a_all, b_all)
post = stats.beta(a, b)
posterior_mean = a / (a + b)
posterior_sd = post.std()
事後分布の広がりも見ておきます。\( \mathrm{Beta}(11, 289) \) の標準偏差は約1.1ポイントで、事前分布の1.7ポイントから狭まりました。データが増えるほど分布は細くなり、推定の不確かさが減っていきます。この広がりをどう要約して報告するか、点推定と区間をどう選ぶかは第3章で扱います。ここでは「事後分布は一点ではなく幅を持った答えであり、その幅自体が更新のたびに縮んでいく」という挙動だけ押さえておけば十分です。
先ほどの重み付き平均の式は、事前分布の影響力がどう変化するかをそのまま示しています。事前分布にかかる重みは \( (a+b)/(a+b+n) \) で、データ量 \( n \) が増えるほど小さくなります。同じ事前分布 \( \mathrm{Beta}(3,97) \) を使い、観測されたコンバージョン率が常に4.0%だったとして、データ量だけを変えてみます。
| データ量 | データ上の比率 | 事後分布 | 事後平均 | 事前分布の重み |
|---|---|---|---|---|
| 10人中1人 | 10.0% | Beta(4, 106) | 3.64% | 90.9% |
| 200人中8人 | 4.0% | Beta(11, 289) | 3.67% | 33.3% |
| 2,000人中80人 | 4.0% | Beta(83, 2017) | 3.95% | 4.8% |
| 20,000人中800人 | 4.0% | Beta(803, 20017) | 3.99% | 0.5% |
10人中1人という観測は、そのまま読めば10%です。この数字を経営会議に持ち込めば、期待値の設定を誤らせます。ベイズ更新は、この観測が持つ情報量が事前の見込みに比べて小さいことを織り込んで、結論を3.64%に留めます。逆に2万人分のデータが集まると、事前分布の重みは0.5%まで下がり、事後平均は3.99%とデータ上の比率にほぼ一致します。事前分布に何を置いたかは、結論にほとんど影響しなくなります。
この挙動は、実務上二つの意味を持ちます。一つは、データが乏しい局面ほど、事前分布に何を置くかが結論を左右するということです。新規サービスの初週、稀にしか起きない故障、テスト回数が限られる領域では、事前分布の設定が実質的な判断になります。もう一つは、データが十分にある局面では、事前分布の選択に神経質になる必要はないということです。「事前分布の選び方で結論が変わってしまうのではないか」という懸念は、データ量によって重みが変わることを踏まえて考える必要があります。
もう一点、しばしば見落とされる効果があります。少数データの極端な値が緩和されるという性質です。10人中0人だった場合、そのまま計算すれば推定値は0%になり、「絶対に申し込まれない」という結論になってしまいます。ベイズ更新なら \( \mathrm{Beta}(3, 107) \) となり、事後平均は約2.7%です。0%や100%という、次の意思決定を壊すような極端な値が出てこないことは、少数データを扱う現場では実用上の利点になります。

では、事前分布には何を置けばよいのか。実務で使われる考え方は、大きく三つに分かれます。
第一は、できるだけ情報を入れない事前分布です。ベータ分布であれば \( \mathrm{Beta}(1,1) \) が0から1まで一様な分布になり、「どの値も等しくありうる」という立場を表します。これを一様事前分布と呼びます。ほかに、パラメータの表し方を変えても主張が変わらないように作られたジェフリーズ事前分布があり、二項分布の場合は \( \mathrm{Beta}(0.5, 0.5) \) になります。この種の事前分布は無情報事前分布と総称されます。
ただし、無情報という呼び名は正確ではありません。\( \theta \) について一様であることは、\( \theta/(1-\theta) \)(オッズ)や \( \log\{\theta/(1-\theta)\} \)(対数オッズ)について一様であることを意味しません。ある表し方で「情報を入れていない」ことは、別の表し方では特定の主張をしていることになります。加えて、面積が無限大になる事前分布(変則事前分布)を使うと、事後分布が確率分布として成立しない場合があります。無情報という言葉を「中立で安全」と読むと、判断を誤ります。
第二は、確実に分かっている常識の範囲だけを入れる 弱情報事前分布 です。コンバージョン率であれば、50%を超えることは業務上まず考えられません。この程度の緩い制約を分布の形で入れておく、という発想です。先ほどの \( \mathrm{Beta}(3, 97) \) は、平均3%という点では主張が強めですが、\( a+b = 100 \) と観測100件相当に留めることで、データが数百件集まれば覆される強さに抑えてあります。回帰モデルであれば、説明変数を標準化したうえで係数に平均0・標準偏差2.5程度の正規分布やコーシー分布を置く形がよく使われ、Stanの事前分布に関する推奨事項でもこの方針が示されています。極端な値を排除しつつ、データが語る余地は残す、という設計です。
弱情報事前分布は、現在のベイズモデリングにおける実質的な標準です。無情報を目指すよりも、常識の範囲を明示的に書いたほうが、推定は安定し、計算も収束しやすくなります。とくに階層モデルや説明変数の多いモデルでは、無情報に近い事前分布を置くと計算が不安定になったり、現実にはありえない値の領域に推定が引っ張られたりします。
第三は、過去の実績や外部知見をしっかり入れる情報事前分布です。同じ製品ラインの過去10回のキャンペーン実績がある、他拠点で同じ設備の故障率が分かっている、といった場合に、その情報を事前分布として持ち込みます。データが少ない領域では、これが最も有効な手段になります。ただし、持ち込んだ根拠を記録に残し、後から検証できる形にしておくことが前提です。
選び方の目安として、事前予測チェックという方法があります。事前分布からパラメータの値をランダムに取り出し、それを使って架空のデータを生成してみる、という手順です。生成されたデータが、業務の常識から見て明らかにおかしい範囲に散らばるなら、その事前分布は緩すぎるか、あるいは間違った方向に強すぎます。コンバージョン率の例なら、事前分布から生成した架空の申込率が80%や90%を頻繁に出すようであれば、その事前分布は現実を表していません。次のコードは、その確認をベータ事前分布に対して行うものです。
import numpy as np
from scipy import stats
rng = np.random.default_rng(0)
def prior_predictive(a, b, n_visitors, n_sim=10000):
"""事前分布から架空のコンバージョン数を生成して分布を見る"""
theta = stats.beta(a, b).rvs(size=n_sim, random_state=rng)
return stats.binom(n_visitors, theta).rvs(random_state=rng)
candidates = {
"一様 Beta(1,1)": (1.0, 1.0),
"ジェフリーズ Beta(0.5,0.5)": (0.5, 0.5),
"弱情報 Beta(3,97)": (3.0, 97.0),
"強め Beta(30,970)": (30.0, 970.0),
}
for label, (a, b) in candidates.items():
sim = prior_predictive(a, b, n_visitors=200)
lo, hi = np.percentile(sim, [2.5, 97.5])
# 生成された申込数の範囲が業務常識に照らして妥当かを目視で確認する
ベイズ統計を業務に持ち込もうとすると、ほぼ必ずこの指摘を受けます。分析者が事前分布を自由に決められるなら、結論も自由に操作できてしまうのではないか、という懸念です。この指摘には正面から答える価値があります。答えは四つに分かれます。
第一に、事前分布は明示されており、検証できます。事前分布は式の形で書き下され、レポートに記載され、後から誰でも「その仮定は妥当か」を議論できます。分析における仮定のうち、これほどはっきり目に見える形で置かれるものは多くありません。頻度論の分析にも、母集団分布の形、独立性、等分散性、外れ値の除外基準、変数の選択といった多数の仮定が入りますが、それらは論文や報告書の本文に埋もれ、明示されないことも珍しくありません。ベイズ統計の事前分布は、隠れた主観を新たに持ち込むものではなく、もともとあった主観のうち一部を、目に見える場所へ引き出すものだと捉えるのが実情に近いといえます。
第二に、結論が事前分布にどれくらい依存しているかは、測ることができます。これが 感度分析 です。事前分布を複数用意して同じデータで更新し、結論がどれだけ動くかを見ます。先ほどの200人中8人のデータに、四つの事前分布を当ててみます。
| 事前分布 | 性格 | 200人中8人での事後平均 | 2,000人中80人での事後平均 |
|---|---|---|---|
| Beta(1, 1) | 一様(無情報) | 4.46% | 4.05% |
| Beta(0.5, 0.5) | ジェフリーズ | 4.23% | 4.02% |
| Beta(3, 97) | 弱情報(平均3%、100件相当) | 3.67% | 3.95% |
| Beta(30, 970) | 強め(平均3%、1,000件相当) | 3.17% | 3.67% |
200人の段階では、事後平均は3.17%から4.46%まで、約1.3ポイントの幅で動きます。事前分布の選び方が結論に効いている状態です。2,000人になると幅は3.67%から4.05%の約0.4ポイントに縮み、どの事前分布を選んでも同じ意思決定に至る可能性が高くなります。「事前分布に依存しているかどうか」は感覚の問題ではなく、この表のように数字で示せる事柄です。依存していれば、そう報告すればよいのです。
第三に、依存していると分かった場合、それはベイズ統計の欠点ではなく、データが足りないという事実の表明です。200人分のデータで事前分布の選択が結論を左右するなら、その状況では「データだけからは決まらない」というのが正しい認識です。頻度論で同じデータを扱えば4.0%という一点が返ってきますが、その数字が事前情報を無視した結果であることは表示されません。事前分布に依存していることが見えるのは、判断材料が増えているということです。
第四に、実務の慣行が主観の暴走を抑えています。弱情報事前分布を既定とし、極端に強い事前分布を使う場合は根拠を明記し、感度分析を報告に添える。この三点を運用ルールとして定めておけば、「事前分布で結論を操作する」余地はほとんど残りません。むしろ、事前分布を明示する必要があるからこそ、「この施策の効果はこのくらいだろう」という組織内の暗黙の期待値が言語化され、議論の対象になります。
なお、無情報事前分布を選べば批判を回避できる、という考え方は勧められません。前述のとおり無情報は表し方に依存し、モデルによっては計算を不安定にします。批判に対する答えは、情報を入れないことではなく、入れた情報を明示し、その影響を測って示すことです。
ここまでの内容を、実際の分析手順としてまとめます。
手順6と7を省略しないことが要点です。事後分布の数字だけを報告すると、受け手はそれを確定値として受け取ります。事前分布に何を置き、それを変えると結論がどう動くかまで示して、はじめて意思決定に使える情報になります。
また、更新の記録を残しておくことも実務上は重要です。いつの時点でどのデータを使い、どの事前分布から出発して、どの事後分布に至ったか。逐次更新を運用する場合、この履歴がないと、数字が変わった理由を後から説明できなくなります。事後分布のパラメータは数個の数値ですから、記録の負担はほとんどありません。
本章では、共役の組み合わせに絞って更新の中身を見てきました。事前分布と尤度を掛けて事後分布を得るという構造、データ量に応じて事前情報とデータが按分されるという挙動、そして逐次更新の整合性は、共役でないモデルでもそのまま成り立ちます。変わるのは計算の方法だけです。得られた事後分布をどう要約し、どう報告するかは次章で、共役でない場合にどう計算するかは第4章で扱います。
『基礎からのベイズ統計学 ハミルトニアンモンテカルロ法による実践的入門』(豊田秀樹編著、朝倉書店):事前分布・尤度・事後分布の関係を、共役の計算を含めて丁寧に追える入門書です。本章で扱ったベータ分布と二項分布の更新を、数式の途中経過まで確認したい場合に向いています。
『ベイズ推論による機械学習入門』(須山敦志、講談社):共役事前分布を軸に、指数型分布族の各組み合わせで事後分布がどう導かれるかを体系的に示した一冊です。二項とベータ、ポアソンとガンマ、正規と正規といった対応を横断的に把握したい方に適しています。
前章までで、事前分布と尤度を掛け合わせて事後分布を得るところまでが揃いました。ベイズ推定の理屈のうえでは、事後分布が得られた時点で推定は完了しています。データから分かることはすべて事後分布の形の中に入っており、それ以上でもそれ以下でもありません。
ところが実務では、事後分布そのものを渡されても仕事は進みません。稟議書に貼るには数字が1つ要りますし、発注量を決めるには「いくつ」という具体値が要ります。会議で説明するには幅が要りますし、来月の在庫を決めるには「次に何個売れそうか」という別の量が要ります。つまり事後分布は、答えではなく答えの原材料です。原材料から実務で使える形に加工する工程が、この章の主題になります。
加工の出口は大きく4つに分かれます。1つ目は分布を1つの数値に要約する点推定、2つ目は幅で示す区間推定、3つ目はまだ観測していないデータの分布を出す予測、4つ目はそれらを踏まえて具体的な行動を1つ選ぶ意思決定です。この4つは別々の技法ではなく、すべて同じ事後分布に対して異なる問いを投げているだけです。順に見ていきます。
なお本章では、事後分布はすでに手元にあるものとして話を進めます。事後分布を実際に計算する手段、とくに数式で解けない場合に用いる乱数計算については第4章で扱います。ここでは、事後分布が数式で書ける単純な例と、事後分布からの乱数(サンプル)が手元にある状況の両方を想定します。実務ではほとんどの場合、後者、つまり事後分布から取り出した数万個の数値の集まりとして事後分布を扱うことになります。
点推定とは、分布という広がりを持った対象を、たった1つの代表値に置き換える操作です。情報を捨てる操作なので、捨て方に何通りかの流儀があります。ベイズ推定でよく使われるのは次の3つです。
1つ目は事後平均で、事後分布の期待値 \( \hat{\theta}_{\mathrm{mean}} = \int \theta \, p(\theta \mid y) \, d\theta \) です。分布の重心にあたります。2つ目は事後中央値で、事後分布を面積で半分に割る点 \( \int_{-\infty}^{\hat{\theta}_{\mathrm{med}}} p(\theta \mid y) \, d\theta = 0.5 \) です。3つ目はMAP推定(最大事後確率推定)で、事後分布の密度がもっとも高くなる点 \( \hat{\theta}_{\mathrm{MAP}} = \arg\max_{\theta} p(\theta \mid y) \) です。事後分布の山の頂上を指すので、事後最頻値と呼ぶこともあります。
具体例で違いを見ます。ある機能を新しく導入したページで、50人が訪問して32人が申し込んだとします。申込率を \( \theta \) とし、事前分布に一様分布、つまり \( \mathrm{Beta}(1,1) \) を置くと、事後分布は \( \mathrm{Beta}(1+32,\ 1+18) = \mathrm{Beta}(33,19) \) になります。この形なら3つの点推定は手計算で出せます。事後平均はベータ分布の期待値の公式から \( 33/(33+19) = 33/52 \approx 0.635 \)、MAP推定は最頻値の公式から \( (33-1)/(33+19-2) = 32/50 = 0.64 \)、事後中央値はこの2つの間に来ます。データだけから素直に計算した割合 \( 32/50 = 0.64 \) と、MAP推定が一致しているのが分かります。一様な事前分布のもとでは、MAP推定は最尤推定と一致します。
この例では3つの値がほとんど変わりません。事後分布が左右対称に近い山の形をしているからです。データが多く集まり、事後分布が正規分布に近づいてくると、どれを選んでもほぼ同じ数字になります。したがって、データが潤沢にある局面では点推定の選び方に神経質になる必要はありません。
問題になるのは、分布が歪んでいるときと、山が複数あるときです。故障間隔や滞在時間、売上金額のように下限があって右に長く裾を引く量では、事後分布も右に歪みます。このとき事後平均は裾に引っ張られて大きめの値になり、MAP推定は山の位置なので小さめの値に留まり、中央値はその間に入ります。3つの値が2割も3割もずれることは珍しくありません。どれが正しいかという問いには意味がなく、どれを使うべきかは後述する損失の形で決まります。
山が複数ある場合はさらに注意が要ります。たとえば顧客が2つのグループに分かれていて、事後分布が2つの山を持つとき、事後平均は2つの山の間の谷、つまりほとんど起こりそうにない値を指します。平均が代表値として機能しない典型例です。この場合は点推定で報告すること自体をやめ、分布の形をそのまま示すか、グループを分けてモデルを組み直すのが正しい対処になります。点推定を出す前に事後分布の形を必ず目で見る、というのは実務上の重要な習慣です。
もう1つ、変換に対する振る舞いの違いも押さえておく価値があります。オッズや対数のようにパラメータを変換して報告する場面は多くありますが、事後平均とMAP推定は非線形な変換のもとで一致しません。\( \theta \) の事後平均を対数に直したものと、\( \log \theta \) の事後平均は別の値になります。一方、中央値は単調な変換なら順序が保たれるため、\( \theta \) の中央値の対数と \( \log \theta \) の中央値は一致します。単位を変えて報告する運用がある場合、この性質は地味に効いてきます。
点推定の選択に理論的な決着をつけるのが損失関数という考え方です。真の値が \( \theta \) であるときに \( a \) という値を報告してしまった場合の損を \( L(a, \theta) \) と書きます。真の値は分からないので、損そのものは計算できません。しかし事後分布は手元にあるので、損の期待値なら計算できます。これを事後期待損失と呼び、\( \mathbb{E}[L(a,\theta) \mid y] = \int L(a,\theta) \, p(\theta \mid y) \, d\theta \) と書きます。
そこで、事後期待損失を最小にする \( a \) を報告する、という方針を採ります。これがベイズ推定における点推定の定義です。損失関数の形を変えると、答えが変わります。
| 損失関数 | 式 | 最適な点推定 | 意味合い |
|---|---|---|---|
| 二乗損失 | \( (a-\theta)^2 \) | 事後平均 | 大きく外すことを強く嫌う。外れ幅が2倍になると損は4倍 |
| 絶対損失 | \( |a-\theta| \) | 事後中央値 | 外れ幅にそのまま比例。外れ値の影響を受けにくい |
| 0-1損失 | ぴったり当たれば0、外れれば1 | MAP推定(事後最頻値) | 当たりか外れかだけを見る。惜しさを評価しない |
| 非対称な線形損失 | \( c_u(\theta-a)_+ + c_o(a-\theta)_+ \) | 事後分布の \( c_u/(c_u+c_o) \) 分位点 | 足りない損と余る損が違う場合。後述 |
この対応関係が示しているのは、点推定の選択は統計学の内部で決まる問題ではなく、業務の損得の構造で決まる問題だということです。「平均と中央値のどちらを使うべきか」という問いは、単独では答えが出ません。「その数字を外したとき、どういう形で損をするのか」を先に決めれば、答えは自動的に定まります。
実務でこの視点が効いてくるのは、たとえば納期の見積りです。納期を短く言い過ぎたときの損失(信用の毀損、追加コスト、違約金)と、長く言い過ぎたときの損失(失注、機会損失)は、普通は同じ大きさではありません。同じ事後分布からでも、どちらの損失が大きいかによって報告すべき日数は変わります。慣習的に平均を出しているだけの数字が、実は業務の損得と噛み合っていない、という事態は起こり得ます。

点推定は分布を1点に潰す操作なので、どうしても不確実性の情報が落ちます。そこで幅で示すのが区間推定です。ベイズ推定における区間を信用区間(確信区間、ベイズ信頼区間とも訳されます)と呼びます。
定義は素直です。事後分布のもとで、パラメータがその区間に入る確率が95%になるような区間 \( [L, U] \)、つまり \( \int_{L}^{U} p(\theta \mid y) \, d\theta = 0.95 \) を満たす区間が95%信用区間です。先ほどの申込率の例なら、事後分布 \( \mathrm{Beta}(33,19) \) の2.5%点と97.5%点を求めれば得られます。事後分布からのサンプルが手元にあるなら、サンプルを並べて下から2.5%の位置と97.5%の位置を取るだけです。
重要なのは、この区間について「申込率が \( L \) から \( U \) の間にある確率は95%である」とそのまま言ってよい、という点です。日本語として自然なこの文が、そのまま数学的に正しい主張になっています。ベイズ推定ではパラメータを確率変数として扱っているため、パラメータについての確率を直接述べることが許されるからです。
この素直さは、実務での伝達コストに直結します。統計に馴染みのない相手に区間を説明するとき、多くの人が最初に思い浮かべる解釈がそのまま正解になるので、注釈も言い換えも要りません。会議の場で「95%の確率でこの範囲」と言い切れることの価値は、思っているより大きいものです。
ここで、従来の統計学(頻度論)で使われる信頼区間との違いを整理しておきます。この2つは数値が近くなることが多いにもかかわらず、意味がまったく違います。混同したまま使われている場面が非常に多い箇所でもあります。
頻度論では、真のパラメータは1つに定まった未知の定数であり、確率変数ではありません。確率が乗るのはデータの側です。95%信頼区間とは、「同じ実験を何度も繰り返して、そのたびにこの手続きで区間を作ったとしたら、そのうち95%の区間が真の値を含む」という、手続きの性質を表す言葉です。したがって、いま手元にある1本の区間について「真の値がこの中にある確率は95%」とは言えません。真の値は定数なので、入っているか入っていないかのどちらかであり、確率は0か1のいずれかです。ただし、そのどちらであるかを知る術がない、というのが頻度論の立場です。
この違いは屁理屈ではなく、報告の場面で実害を生みます。「95%信頼区間は5.0%から9.0%です」と書かれた資料を見た経営層が「では8%を超えている確率は」と尋ねたとき、頻度論の枠組みでは正面から答えられません。答えるには別の計算と別の言葉遣いが必要になります。信用区間であれば、事後分布から8%を超える部分の面積を出すだけで答えが出ます。
| 観点 | 信用区間(ベイズ) | 信頼区間(頻度論) |
|---|---|---|
| 何が確率変数か | パラメータ | データおよび区間の両端 |
| 95%の意味 | この区間にパラメータが入る事後確率 | 手続きを繰り返したときに真値を含む割合 |
| 手元の1本について言えること | 入る確率は95%と言える | 入るか入らないかのどちらか、としか言えない |
| 事前分布の要否 | 必要(結果は事前分布に依存する) | 不要 |
| 途中で見ること | いつ見てもよい | 繰り返し見ると誤りの確率が増える |
公平を期すために、ベイズ側の代償も書いておきます。信用区間が素直な解釈を許すのは、事前分布を置いてパラメータに確率を与えたからです。したがって信用区間は事前分布の選択に依存します。データが十分に多ければ事前分布の影響は薄れ、無情報に近い事前分布を使えば信用区間と信頼区間の数値はしばしばほぼ一致しますが、データが少ない局面では事前分布の置き方で区間が動きます。区間を報告するときは、どういう事前分布を置いたか、その置き方を変えると結論が変わるかどうか(感度分析)を併せて確認するのが誠実な運用です。
また、表の最後に挙げた「途中で見ること」の違いは、A/Bテストの運用で効いてきます。頻度論の枠組みでは、有意差が出るまで毎日結果を覗いて、出た時点で打ち切る運用は誤りを増やします。ベイズの事後分布は、その時点までのデータを条件とした分布であって、いつ見ても解釈は変わりません。この論点は第6章で詳しく扱います。
95%の面積を持つ区間は1つに定まりません。切り取り方に自由度があるからです。実務でよく使われるのは2種類です。
1つは等裾区間で、両側の裾を2.5%ずつ切り落として残りを取る方法です。分位点を求めるだけなので計算が簡単で、サンプルを並べ替えれば済みます。もう1つは最高密度区間(HDI)で、同じ95%の面積を持つ区間のうち幅がもっとも狭くなるものを取る方法です。区間の内側の点はすべて、外側のどの点よりも事後密度が高いという性質を持ちます。
事後分布が左右対称なら両者はほぼ一致します。違いが出るのは分布が歪んでいるときと、パラメータの取りうる範囲に端があるときです。たとえば故障率のように0以上という制約があり、事後分布の山が0のすぐ近くにある場合、等裾区間は下側の2.5%を機械的に切るため区間の左端が0より大きい値になります。実際にはもっとも起こりやすいのが0付近だとしても、その0付近が区間から外れてしまうわけです。最高密度区間ならこうした場合に区間が0から始まり、分布の形と整合します。
一方で最高密度区間には弱点もあります。前述の非線形変換に対して不変ではないので、\( \theta \) の最高密度区間を対数に直したものは \( \log \theta \) の最高密度区間になりません。等裾区間は分位点ベースなので単調変換に対して不変です。単位を変えて報告する運用があるなら等裾区間、分布の形をそのまま伝えたいなら最高密度区間、という選び方になります。どちらを使ったかを注記しておくだけで、後から数字を突き合わせるときの混乱を減らせます。
区間と併せて覚えておくと便利なのが、閾値を跨ぐ確率をそのまま出す報告です。「改善率の95%信用区間は1%から9%」と伝えるより、「改善率が3%を超える事後確率は78%」と伝えるほうが、意思決定の会議では通りが良い場面があります。事業側が気にしているのは区間の端の数字ではなく、投資判断の基準線を越えるかどうかだからです。事後分布さえあれば、基準線を任意に動かして確率を出し直せます。この柔軟さは、区間という固定的な報告形式よりも実務との相性が良いことがあります。
ここまではパラメータについての話でした。しかし実務が知りたいのは、多くの場合パラメータそのものではありません。来月何件申し込みがあるか、次のロットで不良が何個出るか、明日の問い合わせが何件来るか、という観測される量です。これを扱うのが事後予測分布です。
定義は、まだ観測していないデータ \( \tilde{y} \) について、\( p(\tilde{y} \mid y) = \int p(\tilde{y} \mid \theta) \, p(\theta \mid y) \, d\theta \) と書きます。パラメータが \( \theta \) だったときの新しいデータの分布を、事後分布で重み付けして平均したものです。言い換えると、パラメータがどれくらい分かっていないかを織り込んだうえで、次のデータの散らばりを描いた分布です。
ここで押さえるべきは、事後予測分布の広がりが2種類の不確実性の合成になっている点です。1つはパラメータがまだ確定していないことによる広がり、もう1つは、たとえパラメータが完全に分かっていたとしても残る、データそのもののばらつきです。申込率が正確に0.635だと分かっていても、次の100人のうち何人が申し込むかは毎回変わります。この2つを合わせているところに事後予測分布の実務的な意味があります。
これと対比されるのが、点推定を1つ選んでそれを真値とみなす予測です。申込率を0.635と決め打ちして二項分布を引く、という手順は簡単ですが、パラメータ側の不確実性を丸ごと捨てているため、予測の幅が実際より狭くなります。データが少ない局面ほどこの差は大きくなります。予測区間を狭く見積もった結果として在庫や人員を過少に構える、というのは業務上の実害に直結します。パラメータの不確実性を予測に持ち込めることは、ベイズ推定を実務で使う理由の中でも大きいものです。
先ほどの申込率の例で言えば、事後分布 \( \mathrm{Beta}(33,19) \) のもとで次の100人のうち何人が申し込むかの分布は、ベータ分布と二項分布を合成したベータ二項分布になります。この分布は、申込率を0.635に固定した二項分布よりも広がりが大きくなります。計算は数式でもできますが、実務では事後分布のサンプルを使う方法が汎用的です。事後分布から \( \theta \) を1つ引き、その \( \theta \) で二項分布から1つ引く、という操作を繰り返せば、事後予測分布からのサンプルが得られます。
import numpy as np
rng = np.random.default_rng(0)
# 事後分布からのサンプル(共役なので直接引ける。一般にはMCMCの出力を使う)
n_success, n_trial = 32, 50
post_theta = rng.beta(1 + n_success, 1 + (n_trial - n_success), size=100000)
# 点推定
post_mean = post_theta.mean()
post_median = np.median(post_theta)
# 等裾の95%信用区間
ci_low, ci_high = np.quantile(post_theta, [0.025, 0.975])
# 閾値を超える事後確率
prob_over = (post_theta > 0.60).mean()
# 事後予測分布(次の100人のうち何人が申し込むか)
pred = rng.binomial(n=100, p=post_theta)
pred_low, pred_high = np.quantile(pred, [0.025, 0.975])
# 比較用:点推定を真値とみなしたプラグイン予測
plugin = rng.binomial(n=100, p=post_mean, size=post_theta.size)
plugin_low, plugin_high = np.quantile(plugin, [0.025, 0.975])
最高密度区間は分位点だけでは求まらないので、サンプルを並べ替えて、指定した割合を含む窓のうち幅が最小のものを探す方法が簡便です。連続で単峰な分布なら、この手続きで十分に実用的な近似が得られます。
def hdi(samples, prob=0.95):
x = np.sort(np.asarray(samples))
n = x.size
k = int(np.floor(prob * n)) # 区間に含めるサンプル数
widths = x[k:] - x[:n - k] # 幅がk個ぶんの窓をすべて作る
i = int(np.argmin(widths)) # もっとも狭い窓を選ぶ
return x[i], x[i + k]
事後予測分布には、モデルの妥当性を点検する使い道もあります。手元にあるデータを、事後予測分布から生成した仮想データと見比べて、実データが仮想データの散らばりの中に自然に収まっているかを見る方法です。実データだけが極端な位置にあるなら、モデルが現実の構造を取りこぼしていることになります。この点検は事後予測チェックと呼ばれ、モデルを組む工程では標準的な作業になっています。
点推定と予測が揃ったところで、意思決定に接続します。実務の判断が統計の教科書と食い違いやすいのは、外したときの損が上下で対称でない場合です。
代表例が在庫です。ある商品の来月の需要 \( D \) を予測し、発注量 \( a \) を決める場面を考えます。需要が発注量を上回った場合、売り逃した1個あたり \( c_u \) の機会損失が出ます。逆に発注量が需要を上回った場合、余った1個あたり \( c_o \) の在庫コスト(保管費、値引き、廃棄)が出ます。損失関数は \( L(a, D) = c_u \max(D – a, 0) + c_o \max(a – D, 0) \) と書けます。
この期待損失を最小にする発注量は、需要の事後予測分布の \( c_u / (c_u + c_o) \) 分位点になることが知られています。品切れの損失と在庫の損失が同じ大きさなら分位点は0.5、つまり中央値です。品切れの損失が在庫コストの3倍なら分位点は0.75になり、中央値よりかなり多めに発注するのが最適になります。逆に、生鮮品のように余ったときの廃棄コストが大きく、品切れの機会損失が小さい商品なら、分位点は0.5より小さくなり、控えめに発注するのが最適です。
ここで注目すべきは、平均も中央値も出てこない場面がある、という事実です。同じ需要予測から出発しても、商品の損得の構造が違えば、報告すべき数字は分布の別の位置になります。「予測値」という言葉を一律に平均の意味で使っている組織では、この違いが表に出てきません。分布として予測を持っていれば、商品ごとに違う分位点を機械的に取り出せます。
損失関数が単純な形でない場合も、事後予測分布のサンプルさえあれば数値的に解けます。候補となる行動をいくつも並べ、それぞれについてサンプル全体での平均損失を計算し、もっとも小さいものを選ぶだけです。
def expected_loss(action, demand_samples, cost_under, cost_over):
shortage = np.maximum(demand_samples - action, 0)
surplus = np.maximum(action - demand_samples, 0)
return (cost_under * shortage + cost_over * surplus).mean()
# 需要の事後予測サンプル demand_samples に対して、発注量の候補を総当たりで評価する
candidates = np.arange(0, 300)
losses = [expected_loss(a, demand_samples, cost_under=300, cost_over=100)
for a in candidates]
best_action = candidates[int(np.argmin(losses))]
この枠組みは在庫以外にも広く当てはまります。設備保全では、部品交換が早すぎたときの損(まだ使えた部品の廃棄)と遅すぎたときの損(突発停止による生産ロス)が桁で違うことがあります。与信では、貸し倒れの損失と機会損失の非対称性がそのまま審査基準に効きます。人員配置では、人手不足による品質低下と余剰人員の人件費が非対称です。いずれも「予測を当てる」ことが目的ではなく、「損の期待値を小さくする」ことが目的であり、後者を直接扱えるのがこの枠組みの利点です。
実務でこれを導入するときの難所は、統計側ではなく \( c_u \) と \( c_o \) を決める側にあります。機会損失をいくらと見積もるかは、部門によって意見が割れる話です。ただし、その議論は本来決めるべきことを決める議論であり、統計の技法で回避すべきものではありません。むしろ損失の比率を明示的に置く運用にすると、これまで暗黙のうちに個人の勘で調整されていた部分が数値として表に出て、議論の土台になります。

最後に、この章の内容が経営の実務にどう効くかを整理します。技法としては点推定と区間と予測分布と損失関数ですが、組織にもたらす変化は報告の様式そのものにあります。
第1に、判断の可逆性を設計できるようになります。点推定だけで報告された数字は、当たるか外れるかの二択に見えます。区間で報告されていれば、「悪い側の端が出た場合にどうするか」を先に考えられます。撤退ラインを引く、段階投資にする、追加検証の条件を決めるといった打ち手は、幅の情報がなければ設計できません。
第2に、意思決定の基準線と直接つながります。事業側が持っているのは通常、「投資回収に必要な改善率」のような閾値です。事後分布があれば、その閾値を超える確率を即座に出せます。基準線が変わっても、分布を持っていれば計算し直すだけです。統計側で決めた有意水準に事業判断を合わせるのではなく、事業側の基準に統計側が合わせられるようになります。
第3に、追加のデータ収集に投資すべきかを判断できます。区間の幅は、いまの不確実性の大きさを表しています。幅が広く、なおかつ幅の中に判断が変わる分岐点が入っているなら、追加データには価値があります。逆に、区間全体が分岐点の片側にあるなら、これ以上データを集めても判断は変わらないので、収集を止めて次に進むのが合理的です。この判断は点推定だけでは下せません。「あと何件集めれば決められるのか」という問いに答えられることは、分析の投資判断そのものに関わります。
第4に、外れたときの説明の質が変わります。点推定だけを出していた場合、実績がずれたときに残るのは予測が外れたという事実だけです。幅を添えて報告していれば、実績が想定の範囲内だったのか、範囲を超えていたのかを区別できます。範囲内なら想定どおりの変動であり、モデルを直す必要はありません。範囲を超えていたなら、モデルか前提のどこかが間違っていた可能性が高く、見直す理由になります。この区別ができるかどうかで、分析への信頼の積み上がり方が変わってきます。
第5に、モデルの限界が可視化されます。区間が異様に広ければ、データが足りないか、モデルがデータの構造を捉えられていないかのどちらかです。点推定だけを見ていると、まったく根拠のない数字も、十分な根拠のある数字も、同じ見た目で並んでしまいます。幅を必ず添える運用にすると、根拠の薄い数字が自然に目立つようになります。
実務の報告フォーマットとしては、点推定を1つ、区間を1つ、経営の基準線を超える確率を1つ、そして置いた前提の一覧、という組み合わせが扱いやすいものになります。点推定は会議の場で数字を1つ言うために要りますし、区間はリスクの合意のために要りますし、確率は判断のために要ります。前提の一覧は、事前分布とモデルの選択が結論をどれだけ動かすかを確認するために要ります。
注意点も添えておきます。信用区間も事後予測分布も、置いたモデルが正しいという条件のもとでの確率です。モデルが現実の構造を取りこぼしていれば、区間は実態より狭く出ます。区間が狭いことは、正しさの証明ではありません。区間を報告するときは、事前分布を変えたときに結論が動くか、モデルの形を変えたときに動くかを併せて確認し、動く場合はその旨を明記するのが誠実な扱いになります。
ここまでで、事後分布さえ手に入れば実務の答えを取り出せることが確認できました。残る問題は、その事後分布をどう手に入れるかです。この章の例では事前分布と尤度の組み合わせが都合よく、事後分布が数式で書けました。しかし現実のモデルではこの計算がほとんど不可能になります。その壁と、壁を越えるための計算手法が、次章の主題です。
『ベイズ統計モデリング R,JAGS,Stanによるチュートリアル 原著第2版』(John K. Kruschke著、前田和寛・小杉考司監訳、共立出版):最高密度区間や実質的同等領域といった、区間をどう報告し、どう判断につなげるかという論点を丁寧に扱っています。本章で触れた区間の選び方をさらに掘り下げたい場合に適しています。
『ベイズ推論による機械学習入門』(須山敦志著、杉山将監修、講談社):事後分布と予測分布を数式で丁寧に追う構成で、事後予測分布がなぜパラメータの不確実性を織り込めるのかを式のレベルで理解したい方に向いています。
ベイズ推定は、事前分布と尤度を掛け合わせて事後分布を得るという一本の筋で説明できます。ところが実際にモデルを組もうとすると、その事後分布が数式の形で書き下せないという事態にすぐ突き当たります。書き下せないものをどうやって使うのか。この問いに対する現代の標準的な答えが、マルコフ連鎖モンテカルロ法、いわゆるMCMCです。この章では、なぜ解析的に解けないのか、MCMCは何をしているのか、そして計算が終わったときに何が保証されていて何が保証されていないのかを、実務の担当者が自分の言葉で説明できる水準まで整理します。
ここを飛ばしてもモデルは動きます。StanやPyMCに式を渡せば結果は返ってきます。ただし返ってきた結果が信用できるかどうかを判断する材料は、この章の内容の中にあります。収束していない計算結果は、見た目には収束した結果と区別がつきません。数字が出てくること自体は、正しさの証拠にならないためです。
前章までに扱った例では、事後分布が既知の分布として素直に求まりました。コイン投げのような二値データにベータ分布の事前分布を置けば事後分布もベータ分布になり、正規分布の平均に正規分布の事前分布を置けば事後分布も正規分布になります。この性質のおかげで、事後平均も区間も式に代入するだけで得られました。
しかしこれは、事前分布と尤度の組み合わせを意図的に都合よく選んだ結果です。データの構造が少し複雑になった途端、この都合のよさは失われます。たとえば、成約するかしないかという二値の結果を、来訪回数と提案金額と担当者の経験年数という三つの説明変数で説明したいとします。ロジスティック回帰の尤度は、各観測について \( \sigma(\beta_0 + \beta_1 x_{1} + \beta_2 x_{2} + \beta_3 x_{3}) \) というシグモイド関数の値を確率として使う形になります。ここに回帰係数の事前分布として正規分布を置くと、事後分布は「シグモイド関数の積と正規分布の密度の積」という形になります。この形は、名前のついた確率分布のどれにも一致しません。
同じことは、複数の地域や店舗をまとめて扱う階層構造のモデル、複数の集団が混ざっているとみなす混合モデル、感染者数の推移を微分方程式で表す力学モデルなど、実務で価値のあるモデルのほとんどで起こります。むしろ、事後分布が既知の分布に収まるのは、モデルを単純にした特別な場合だけだと考えたほうが実態に近くなります。
ベイズの定理をパラメータ \( \theta \) とデータ \( D \) で書くと、次の形になります。
\( p(\theta \mid D) = \dfrac{p(D \mid \theta)\, p(\theta)}{p(D)} \)
右辺の分子は、尤度と事前分布の積です。これはモデルを決めた時点で式が確定しているので、任意の \( \theta \) に対して値を計算できます。困るのは分母です。分母は
\( p(D) = \displaystyle\int p(D \mid \theta)\, p(\theta)\, d\theta \)
という積分で、パラメータのあらゆる値にわたって分子を足し上げたものです。これは 正規化定数 と呼ばれ、事後分布の全体の面積を1に揃えるための割り算の相手にあたります。分子の形が単純なら手で積分できますが、シグモイド関数の積のような形になると、原始関数が存在しないため式として解けません。
解けないなら数値的に足し上げればよい、という発想は自然です。パラメータが一つなら、その範囲を100個の格子点に区切って分子を計算し、合計すれば十分な精度が出ます。パラメータが二つなら格子点は100×100で1万個です。三つなら100万個です。ここまでは計算機の仕事として現実的です。ところが実務のモデルでパラメータが10個になると格子点は100の10乗、すなわち1兆の1万倍という数になります。階層モデルでは地域や店舗の数だけパラメータが増えるので、100個や1000個は珍しくありません。格子を切るという方法は、次元が増えると急速に破綻します。これが次元の呪いと呼ばれる現象です。
さらに悪いことに、格子で足し上げる方法は無駄が多くなります。事後分布の確率の大部分は、パラメータ空間のごく狭い領域に集中しています。データが増えるほどこの集中は強まります。均等に切った格子の大半は、ほとんどゼロの値を計算するためだけに使われることになります。計算資源を、答えに寄与しない場所に配り続けているわけです。

ここで発想を切り替えます。実務で事後分布に対して行いたいことを並べてみると、次のようなものになります。パラメータの代表値を出す、区間を出す、ある係数が正である確率を出す、二つの施策の効果の差が閾値を超える確率を出す、将来の観測値の予測分布を出す。これらはすべて、何らかの関数 \( f \) について
\( E[f(\theta)] = \displaystyle\int f(\theta)\, p(\theta \mid D)\, d\theta \)
という期待値の計算に還元できます。事後平均なら \( f(\theta) = \theta \)、係数が正である確率なら \( \theta > 0 \) のとき1、そうでないとき0を返す関数です。
そして期待値は、その分布から取ってきた標本の平均で近似できます。事後分布に従う値を \( \theta^{(1)}, \theta^{(2)}, \ldots, \theta^{(N)} \) と \( N \) 個手に入れられれば、
\( E[f(\theta)] \approx \dfrac{1}{N} \displaystyle\sum_{n=1}^{N} f(\theta^{(n)}) \)
という近似が成り立ちます。これがモンテカルロ積分です。大数の法則により、標本数を増やせばこの近似は真の値に近づきます。しかも近似誤差の大きさは標本数の平方根に反比例して減っていくだけで、パラメータの次元には直接依存しません。格子を切る方法が次元とともに破綻するのとは対照的です。
つまり問題は、「事後分布の式を求める」から「事後分布から標本を引く」に置き換わりました。区間を出したければ標本を並べ替えて該当する位置の値を読めばよく、ある条件を満たす確率を出したければ条件を満たす標本の割合を数えればよいことになります。事後分布を、数式ではなく数千行の数値の表として持つという扱い方です。
ただし、ここで新しい困難が現れます。標本を引くには通常、その分布の形が分かっている必要があります。正規分布から乱数を引く関数は用意されていますが、それは正規分布の形が既知だからです。今扱っているのは、分子の値は計算できるが正規化定数が分からない分布です。全体の面積が分からない状態で、面積に比例した頻度で点を選ぶという操作は、素朴には実行できません。
この困難を回避するのがマルコフ連鎖という道具です。マルコフ連鎖とは、次に移る場所が今いる場所だけで決まる確率的な移動のことです。今より前の履歴は影響しません。
マルコフ連鎖には、長く動かし続けたときに落ち着く先の分布が存在する場合があります。どこから出発しても、十分な回数の移動のあとには、どの場所にどれくらいの割合で滞在するかが一定の分布に近づく、という性質です。この落ち着き先を定常分布と呼びます。
MCMCが行うのは、この関係を逆向きに使うことです。すなわち、自分が標本を引きたい事後分布が定常分布になるようなマルコフ連鎖を設計し、それを長時間動かして、通った場所を記録します。連鎖が十分に落ち着いたあとの記録は、事後分布からの標本とみなせます。記録した値のヒストグラムを描けば、それが事後分布の形になります。
この設計が可能であることと、正規化定数を知らなくても設計できることが、MCMCの中心的な発見です。次にその仕組みを見ます。
メトロポリス・ヘイスティングス法 は、MCMCのもっとも基本的な形です。手順は次の三つのステップの繰り返しです。
受理確率は次の式で与えられます。
\( \alpha = \min\left(1,\ \dfrac{p(\theta^{*} \mid D)\, q(\theta \mid \theta^{*})}{p(\theta \mid D)\, q(\theta^{*} \mid \theta)}\right) \)
ここが決定的に重要な点です。この式には事後分布が二回、分子と分母に一回ずつ現れます。事後分布は「分子である尤度と事前分布の積」を「正規化定数」で割ったものですから、比を取ると正規化定数は約分されて消えます。つまり、計算できない正規化定数を一度も計算せずに、受理確率を求められます。求まらないのは面積の絶対値だけで、二点の高さの比なら分子だけで分かる、という構造を利用しているわけです。
提案分布が対称な場合、たとえば今の位置を中心とした正規分布から候補を引く場合には、\( q(\theta \mid \theta^{*}) \) と \( q(\theta^{*} \mid \theta) \) が等しくなるので式はさらに簡単になり、受理確率は事後密度の比そのものになります。候補のほうが密度が高ければ比が1を超えるので必ず移動します。候補のほうが密度が低ければ、その比の確率で移動します。密度が半分の場所へは2回に1回移動する、という具合です。
低いほうへも確率的に移動するという点が、最適化との決定的な違いです。最適化なら値が下がる方向へは進みません。しかしここで欲しいのは頂上の一点ではなく、分布全体の形です。裾の領域にも、その密度に見合った頻度で滞在する必要があります。低い場所へ移る確率を密度の比に一致させることで、長期的な滞在時間の割合が事後密度の比に一致するように調整されています。
棄却されたときの扱いにも注意が必要です。棄却したらその場に留まりますが、そのときも標本を一つ記録します。同じ値が二回並ぶことになります。棄却を「失敗だから記録しない」と扱うと、密度の高い場所に長く留まるという性質が失われ、分布の形が歪みます。留まることも標本の一部です。
Pythonで書くと、骨格は数十行に収まります。次は対称な提案分布を使った素朴な実装の例です。
import numpy as np
def metropolis(log_post, init, n_iter, step, rng):
"""log_post: 事後分布の対数(正規化定数を除いた値でよい)"""
theta = np.asarray(init, dtype=float)
lp = log_post(theta)
samples = np.empty((n_iter, theta.size))
n_accept = 0
for i in range(n_iter):
# 現在地の周りに正規分布を置いて候補を提案する
cand = theta + step * rng.standard_normal(theta.size)
lp_cand = log_post(cand)
# 対数で比を取る。exp を取れば密度の比になる
log_alpha = lp_cand - lp
if np.log(rng.random()) < log_alpha:
theta, lp = cand, lp_cand
n_accept += 1
# 棄却された場合も現在地を記録する
samples[i] = theta
return samples, n_accept / n_iter
実装で確率の比をそのまま扱わず対数に直しているのは、尤度がデータ点の数だけ掛け算になるため、生の値が容易に桁溢れするからです。対数を取れば掛け算が足し算になり、数値的に安定します。
この方法の実用上の急所は、提案の幅にあります。幅を大きく取ると遠くまで一気に動けますが、密度の低い場所ばかり提案することになり、ほとんどが棄却されて連鎖が同じ場所に留まり続けます。幅を小さく取ると提案はほぼ受理されますが、一歩が小さいので分布全体を歩き回るのに膨大な反復が必要になります。どちらに振っても効率が落ちるため、中間の適切な幅を探す作業が発生します。単純な形のモデルでも、この調整は手間のかかる工程です。

受理確率をこの形に決めると、連鎖の定常分布が目標の事後分布に一致することを証明できます。証明の鍵になるのが 詳細釣り合い条件 です。これは、任意の二点 \( a \) と \( b \) について、\( a \) にいて \( b \) へ移る量と、\( b \) にいて \( a \) へ移る量が釣り合うという条件です。式で書くと
\( p(a \mid D)\, T(b \mid a) = p(b \mid D)\, T(a \mid b) \)
となります。\( T \) は連鎖の遷移確率です。この条件が成り立てば、目標分布は連鎖によって変化しません。すなわち定常分布になります。メトロポリス・ヘイスティングス法の受理確率は、この等式が成り立つように逆算して作られた式です。
定常分布が目標分布であることに加えて、二つの条件が必要です。一つは既約性で、パラメータ空間のどの領域にも有限回の移動で到達できることです。もう一つは非周期性で、決まった周期で同じ場所に戻る癖がないことです。この二つが満たされると、エルゴード定理により、初期値がどこであっても標本平均が真の期待値に収束することが保証されます。
ここで、保証の範囲を正確に押さえておく必要があります。保証されているのは「反復を無限に増やせば収束する」ことです。有限回で止めた結果がどれくらい真の値に近いかについては、一般には何も言えません。特に、事後分布に山が複数あり、山と山の間が非常に低い谷で隔てられている場合、理論上はいつか谷を越えられるとしても、現実的な反復回数では一方の山に閉じ込められたままになります。閉じ込められた連鎖は、その山の内側では正常に見えます。滑らかに動き、標本も溜まり、要約統計量も出ます。それが全体の一部でしかないことは、連鎖の内側からは分かりません。
この「無限回なら正しいが、有限回で足りているかは分からない」という性質が、後述する診断の作業が必要になる理由です。MCMCは正しい答えに近づく仕組みを持っていますが、近づき終わったことを自ら教えてはくれません。
ギブスサンプリングは、メトロポリス・ヘイスティングス法の特別な場合として位置づけられる方法です。パラメータが複数あるとき、他のすべてのパラメータを現在の値に固定したうえで、一つのパラメータの条件付き分布から値を引きます。これを全パラメータについて順番に繰り返します。パラメータが \( \theta_1, \theta_2, \theta_3 \) の三つなら、\( \theta_2, \theta_3 \) を固定して \( \theta_1 \) を引き、更新した \( \theta_1 \) と \( \theta_3 \) を固定して \( \theta_2 \) を引き、という手順です。
この方法の利点は、受理確率が常に1になることです。棄却が起こらないため、提案幅の調整という作業がありません。条件付き分布が既知の分布になるモデルでは、素朴で効率のよい方法になります。実際、階層モデルの多くは、全体としては解析的に解けなくても、一つのパラメータだけを取り出した条件付き分布は共役の関係になって既知の分布に収まる、という構造を持っています。この構造を利用できる点が、長らくギブスサンプリングが標準的に使われてきた理由です。
弱点は、パラメータどうしの相関が強いときに現れます。ギブスサンプリングは一度に一つの軸方向にしか動きません。二つのパラメータが強く相関していて、事後分布が斜めに細長い形をしている場合、軸に沿った移動では細長い谷の幅の分しか進めません。斜めの方向に進みたいのに、縦と横の小刻みな移動を延々と繰り返すことになります。この状況では、標本は溜まっていくのに分布全体を回りきれず、見かけ上は動いているのに実質的な情報がほとんど増えないという事態になります。回帰係数どうしが相関する場面や、階層モデルで全体平均と群ごとの偏差が絡み合う場面では、この非効率が現実の問題になります。
提案をでたらめに出す方法は、パラメータの次元が上がるほど不利になります。10次元や100次元の空間で現在地の周りにランダムに候補を出すと、そのほとんどは密度の低い方向に外れます。次元が上がるほど「外れる方向」の割合が増えるためです。結果として受理率が落ち、実用的な時間では動ききれなくなります。
ハミルトニアンモンテカルロ法、略してHMCは、この問題を対数事後密度の勾配を使って解決します。勾配は、密度が増える方向とその急さを教えてくれる情報です。この情報を使えば、当てずっぽうではなく、密度の高い領域に沿った方向へ提案を出せます。
仕組みは物理の運動になぞらえて設計されています。対数事後密度に負号をつけたものを、位置エネルギーの地形とみなします。事後密度が高い場所ほど低い谷になります。ここにパラメータを表す位置 \( \theta \) を置き、補助的な変数として運動量 \( r \) を導入します。全エネルギーは
\( H(\theta, r) = -\log p(\theta \mid D) + \dfrac{1}{2} r^{\top} M^{-1} r \)
と書けます。第一項が位置エネルギー、第二項が運動エネルギーです。\( M \) は質量行列と呼ばれ、各方向の動きやすさを表します。
一回の提案は次のように作られます。まず運動量を正規分布からランダムに引きます。これがボールに与える初速にあたります。次に、この地形の上でボールを転がす運動を、リープフロッグ法という数値積分で一定回数進めます。谷の底に向かって加速し、勢いで反対側の斜面を登り、また戻ってくるという軌道を描きます。この軌道の終点を候補として提案します。
この軌道はエネルギーを保存するため、出発点と終点で事後密度が大きく変わりません。つまり、遠くまで移動しているのに密度の低い場所には落ちていません。これが高い受理率と長い移動距離を両立させる理屈です。ランダムウォークが酔歩のように少しずつしか進めないのに対して、HMCは地形に沿って一気に反対側まで移動できます。
数値積分には誤差が入るためエネルギーは厳密には保存しません。そこで最後にメトロポリス法と同じ受理判定を挟み、誤差の分だけ棄却する仕組みを入れています。これによって、数値誤差があっても目標分布が保たれます。
代償もあります。勾配を計算できることが前提になるため、対数事後密度がパラメータについて微分可能でなければなりません。整数値しか取らない離散パラメータはそのままでは扱えず、和を取って消す変形などが必要になります。また一回の提案の中で勾配計算を何十回も行うので、一反復あたりの計算量はメトロポリス法より格段に重くなります。それでも、同じ精度に到達するまでの総時間では有利になる場面が多く、これが現代のベイズ推定の実行環境で標準的に採用されている理由です。

HMCには、実務上やっかいな二つの設定値があります。リープフロッグ法の刻み幅 \( \varepsilon \) と、何回進めるかというステップ数 \( L \) です。刻み幅が大きすぎると数値誤差でエネルギーが崩れて棄却が増え、小さすぎると計算が無駄に重くなります。ステップ数が少なすぎると結局ランダムウォークに近い動きになり、多すぎると軌道が一周して出発点の近くに戻ってきてしまい、計算した分が無駄になります。
NUTSは、この後者を自動化した方法です。名前はNo U Turn Samplerの略で、軌道がUターンを始めたところで止めるという着想に由来します。具体的には、軌道を前方と後方に倍々に伸ばしながら、両端の位置と運動量の関係を監視し、これ以上進んでも出発点から遠ざからないと判定された時点で伸長を打ち切ります。そのうえで、作られた軌道全体の中から適切な確率で一点を選びます。ステップ数を人が指定する必要がなくなり、しかも場所ごとに必要な長さが変わる状況にも対応できます。
刻み幅のほうは、ウォームアップ期間中に自動調整されます。受理率が目標値に近づくように刻み幅を少しずつ動かす仕組みが働きます。同時に、パラメータごとのスケールの違いを吸収するための質量行列も、ウォームアップ中の標本から推定されます。StanやPyMCで特別な設定をせずにモデルが動くのは、この自動調整が背後で走っているからです。
言い換えると、実行時に指定する反復回数やウォームアップ回数は、単なる計算量の指定ではありません。ウォームアップを極端に短くすると、刻み幅と質量行列の推定が不十分なまま本番の標本収集に入ることになり、効率が大きく落ちます。時間を削りたいときにウォームアップから削るのは、多くの場合逆効果になります。
MCMCは任意の初期値から出発します。初期値が事後分布の密度の高い領域から離れていれば、最初のうちの標本は事後分布からの標本ではありません。連鎖が高密度領域に到達するまでの区間を、バーンインあるいはウォームアップと呼び、通常はこの区間の標本を捨てます。
捨てる理由は二つあります。一つは今述べたとおり、定常分布に達する前の標本は目標分布を代表していないためです。これを混ぜると、初期値の位置に引きずられた要約統計量が出ます。もう一つは、NUTSのような適応的な手法の場合、ウォームアップ期間中は刻み幅や質量行列そのものが変化し続けているためです。遷移のルールが変わっている間の連鎖は、そもそも一定の定常分布を持ちません。したがってウォームアップの標本を捨てるのは、余裕を見るための慣習ではなく、理論上必要な手続きです。
ではどれくらい捨てればよいかというと、事前に確定した答えはありません。実務では、既定値として全反復の半分をウォームアップに割り当てる設定がよく使われます。そのうえで、あとで述べる診断指標を見て足りているかを判断します。初期値を事後分布の高密度領域の近くに置ければウォームアップは短くて済みますが、その場所が事前に分かっているならそもそも推定は要らないため、通常は妥当な範囲の値から出発して連鎖に任せます。
MCMCで得られる標本には、独立な乱数にはない性質があります。隣り合う標本が似ているという性質、すなわち自己相関です。次の値は今の値の近くから提案されるので、当然といえば当然です。棄却が起これば、まったく同じ値が並びます。
この性質があるため、標本の個数をそのまま情報量とみなすことはできません。4000個の標本があっても、隣接する標本がほとんど同じ値なら、独立な乱数に換算すれば数十個分の情報しかないという事態が起こります。この換算後の個数を 有効サンプルサイズ と呼びます。標本の自己相関の強さから推定される量で、実行結果の要約表に表示されます。
事後平均の推定誤差は、有効サンプルサイズを \( N_{\mathrm{eff}} \)、事後分布の標準偏差を \( \sigma \) として、おおよそ \( \sigma / \sqrt{N_{\mathrm{eff}}} \) の大きさになります。この関係から、必要な標本数を逆算できます。事後平均を事後標準偏差の10分の1の精度で出したければ、有効サンプルサイズは100程度あればよいことになります。一方、区間の端のような裾に近い量を安定させたい場合は、中央付近の量よりも多くの標本が必要になります。実務では、報告に使うすべての量について有効サンプルサイズが数百以上あることを確認するのが安全な運用です。
なお、自己相関を減らすために標本を数個おきに間引くという操作が行われることがあります。この操作自体は自己相関を下げますが、捨てた分だけ情報も減るため、推定精度の観点では通常は得になりません。保存領域が足りない場合の妥協策と考えるのが妥当です。
MCMCの計算では、独立した連鎖を複数本、異なる初期値から同時に走らせるのが標準です。既定で4本という設定が多く採用されています。この理由は、収束の判定にあります。
1本の連鎖だけを見ていると、その連鎖が事後分布の全体を回っているのか、一部の領域に閉じ込められているのかを区別できません。閉じ込められた連鎖も、内側では滑らかに動いて見えます。ところが、離れた初期値から出発した複数の連鎖が、最終的に同じ領域を同じように動いているなら、少なくとも「別々の場所に閉じ込められている」という状況ではないと言えます。
これを数値化した指標がR-hatです。連鎖間のばらつきと連鎖内のばらつきを比べ、両者が一致していれば1に近い値を取ります。連鎖ごとに違う場所を回っていれば、連鎖間のばらつきが相対的に大きくなり、1より明確に大きな値になります。実務上は、すべてのパラメータについてR-hatが1.01を下回っていることを目安に確認します。
あわせて、トレースプロットと呼ばれる図を目視します。横軸に反復回数、縦軸にパラメータの値を取り、連鎖ごとに折れ線を描いたものです。複数の連鎖が同じ帯の中で混ざり合い、上下に細かく振動している状態が望ましい姿です。一方の連鎖だけが別の高さで推移している、値が一方向に流れ続けている、長い区間で平らになっているといった様子が見えれば、収束していないか、効率が著しく悪い状態です。
ここでも保証の範囲を正確に述べておく必要があります。R-hatが1に近いことは、収束の証明ではありません。すべての連鎖が同じ一つの山に閉じ込められていれば、連鎖どうしは一致するのでR-hatは1に近くなります。診断指標は問題を検出する道具であって、問題がないことを証明する道具ではありません。したがって、初期値をなるべく散らして出発させることや、モデルの構造から複数の解が生じうるかをあらかじめ考えておくことが、指標の値と同じくらい重要になります。
最小二乗法や最尤法では、方程式を解くか最適化を回すかして、パラメータの一点を求めます。データが数万行あっても、単純なモデルなら一瞬で終わります。これに慣れていると、MCMCが数分から数時間かかることは不可解に見えます。社内で説明を求められる場面もあるはずなので、説明の筋を整理しておきます。
MCMCが求めているのは一点ではなく、分布の形です。分布の形を数千個の点で描くために、事後密度の評価を数千回から数万回繰り返します。HMCの場合はさらに、一回の提案の中で勾配計算を数十回行います。そして事後密度の評価は、データ全件についての尤度の計算を含みます。したがって計算時間は、おおまかに「データ件数 × 反復回数 × 一反復あたりの勾配計算回数」に比例して増えます。最尤法が数十回の反復で終わるのに対して、桁が三つか四つ違う計算をしていることになります。
その対価として得られるものは、パラメータの不確実性がそのまま数値として残ることです。点推定だけを返す方法では、推定値がどれくらい揺れうるかを別途近似で補う必要があります。MCMCの出力はパラメータの組の集まりなので、そのまま予測に流し込めば予測の不確実性まで一貫して伝わります。複数のパラメータにまたがる複雑な条件の確率も、標本を数えるだけで求まります。この一貫性が、待ち時間の見返りです。
実務での折り合いのつけ方としては、開発時にはデータを絞って短い反復で回し、モデルの記述が意図どおりかを確認します。本番の推定では十分な反復数で回し、診断指標をすべて確認したうえで結果を採用します。待てない事情がある場合には、事後分布を正規分布で近似する方法や、最適化問題に置き換えて近似する変分推論といった選択肢もあります。ただしこれらは近似であり、分布の裾や複雑な形状の再現には限界があります。どこまで近似を許容できるかは、その結果をどの意思決定に使うかによって決まります。この判断については、実務適用の設計を扱う章であらためて整理します。
この章の内容を一文にまとめると、MCMCは「解けない積分を、確率的な探索によって標本に置き換える方法」であり、その正しさは無限反復での収束として保証されている一方、有限時間で足りているかどうかは診断指標で確認するしかない、ということになります。実行が終わったときに見るべきものは、パラメータの推定値だけではありません。R-hatと有効サンプルサイズ、そしてトレースプロットを確認して初めて、その推定値を報告に載せてよいかが判断できます。
『データ解析のための統計モデリング入門』(久保拓弥、岩波書店):一般化線形モデルから階層モデルまでを段階的に積み上げる構成で、メトロポリス法とギブスサンプリングが何をしているかを、図と具体例で丁寧に追える一冊です。
『計算統計II マルコフ連鎖モンテカルロ法とその周辺』(伊庭幸人ほか、岩波書店):詳細釣り合いや収束の条件といった理論的な裏づけと、実装上の工夫の両方を扱う専門書です。診断指標の意味を根拠まで遡って理解したい場合の参照先になります。
前章までで、事後分布を数式のうえで直接求めるのは現実的な問題ではほとんど不可能であり、乱数を使って事後分布からの標本を集めるという迂回路が要る、という話をしました。ではその迂回路を、実務でどう歩くのか。ここを担うのが確率的プログラミング言語(Probabilistic Programming Language、以下PPL)と呼ばれる道具です。
PPLが登場する前は、モデルを1つ作るたびに、対数事後分布を手で微分し、条件付き分布を導出し、サンプラーそのものをコードで書く必要がありました。モデルを少し変えれば導出のやり直しになります。この作業量が、ベイズ統計を「理屈は分かるが実務では使いにくい手法」の位置に長くとどめていました。
PPLがしたことは、モデルの記述と推論アルゴリズムの分離です。利用者は「データがどういう確率的な仕組みで生まれたと考えるか」だけを宣言的に書きます。そこから対数事後分布を組み立て、勾配を自動微分で求め、サンプラーを回し、結果を標本の集まりとして返す部分は処理系が引き受けます。数理最適化でモデル記述とソルバーが分離されているのと同じ構図と考えていただければ近いです。
経営判断の観点で言えば、この分離には運用上の意味があります。モデルの中身がコード上の数行として明示的に残るため、なぜその推定になったのかを後から読み返せます。前提を変えたときの影響を、導出の作り直しではなく、その数行の書き換えとして扱えます。分析の属人性を下げる方向に効く構造です。

2026年時点で、実務で名前を見かけるPPLはおおむね次の3つに整理できます。いずれも背後の推論アルゴリズムはハミルトニアンモンテカルロ系の自動調整版であり、同じモデルを書けば同じ事後分布に向かいます。違いは処理系の作りと、周辺の環境です。
| 処理系 | 基盤と呼び出し元 | 特徴 | 向いている場面 |
|---|---|---|---|
| Stan | C++で実装された独立の処理系。専用のモデル記述言語を持ち、RとPythonの双方から呼べる | 統計モデリングの定番。文献・書籍・事例の蓄積が最も厚い。実行前にコンパイルが必要 | 統計の作法に沿ってじっくり組む場面。先行研究や書籍のコードをそのまま参照したい場面 |
| PyMC | Pythonネイティブ。計算グラフはPyTensorが担当 | モデルもデータ整形も同じPythonの中で完結する。pandasやscikit-learnとの往復が楽 | 既存のPythonの分析基盤に組み込む場面。試行錯誤の回数を増やしたい場面 |
| NumPyro | JAXベース。PyTorch系のPyroの姉妹にあたる | JITコンパイルとGPU/TPU利用により、同じモデルでも実行が速いことが多い | データ量やパラメータ数が大きい場面。同じモデルを繰り返し回すバッチ処理 |
選び方の基準は、統計的な優劣ではなく、組織の側の条件で決まることがほとんどです。分析チームがRを主に使っているならStan、Pythonで基盤が組まれているならPyMC、計算時間が律速になっているならNumPyro、という順で考えると外しにくくなります。参照したい書籍や論文の実装がどれで書かれているかも、実際には大きな判断材料です。
もう1つ、覚えておくと役に立つのがArviZというライブラリです。これはPPLそのものではなく、推論結果を共通の形式(InferenceData)で受け取り、診断と可視化を提供する層です。StanでもPyMCでもNumPyroでも、結果をArviZに渡してしまえば、以降の診断コードは同じものが使えます。処理系を乗り換えるときの摩擦を下げる意味でも、診断はArviZに寄せておくのが実務的です。
どの処理系を使う場合でも、作業の流れはほぼ共通です。順に並べると次のようになります。
ここで区別しておきたいのは、4番と6番の性格の違いです。収束診断は「計算が正しく行われたか」の検査であって、「モデルが正しいか」の検査ではありません。まったく的外れなモデルでも、計算そのものは何事もなく収束します。逆に、良いモデルでも書き方が悪ければ収束しません。この2つを混同したまま「収束したから大丈夫」と判断してしまう例は少なくありません。計算の健全性とモデルの妥当性は、別々に確かめる必要があります。
2番の事前予測チェックも、慣れないうちは飛ばされがちですが、費用対効果の高い工程です。事前分布から適当にパラメータを引いて、そのパラメータのもとでデータを生成してみる。出てきた値が、身長が3メートルになるとか、売上が負になるといった、常識に反する範囲を大量に含んでいるなら、その事前分布は広すぎます。事前分布を「情報を入れないために極端に広くする」という発想は、しばしばこの形で裏目に出ます。
具体的な書き方を見るために、最も単純な題材を使います。説明変数\( x \)から目的変数\( y \)を予測する単回帰です。通常の最小二乗法で扱う対象ですが、これをベイズの枠組みで書くと次のようになります。
\( y_i \sim \mathrm{Normal}(\alpha + \beta x_i, \sigma) \)
そして、切片\( \alpha \)、傾き\( \beta \)、観測のばらつき\( \sigma \)のそれぞれに事前分布を置きます。
\( \alpha \sim \mathrm{Normal}(0, 10) \)、\( \beta \sim \mathrm{Normal}(0, 10) \)、\( \sigma \sim \mathrm{HalfNormal}(10) \)
\( \sigma \)は標準偏差なので負にはなりません。そのため正の側だけを持つ分布を割り当てます。半正規分布はその代表で、正規分布の正の側だけを取り出して面積を1に直したものです。
ここで注意すべきなのは、事前分布の広さが変数の単位に依存する点です。\( \beta \)に標準偏差10の正規分布を置いたとして、それが緩い制約なのかきつい制約なのかは、\( x \)と\( y \)がどんな単位で測られているかによって変わります。売上を円で測るのか百万円で測るのかで、同じ「10」の意味がまるで違います。この面倒を避けるため、実務では説明変数を標準化(平均0、標準偏差1に変換)してからモデルに入れるのが一般的です。標準化しておけば、事前分布のスケールを単位から切り離して考えられます。
PyMCでは、モデルをwithブロックの中に書きます。ブロックの中で作った確率変数が、そのモデルに属するものとして登録される仕組みです。
import numpy as np
import pymc as pm
import arviz as az
# x, y は事前に用意した1次元のNumPy配列とする
x_std = (x - x.mean()) / x.std()
with pm.Model() as model:
# 事前分布
alpha = pm.Normal("alpha", mu=0.0, sigma=10.0)
beta = pm.Normal("beta", mu=0.0, sigma=10.0)
sigma = pm.HalfNormal("sigma", sigma=10.0)
# 線形予測子
mu = alpha + beta * x_std
# 尤度(observed に実データを渡した時点で観測値として扱われる)
pm.Normal("y_obs", mu=mu, sigma=sigma, observed=y)
idata = pm.sample(
draws=2000,
tune=1000,
chains=4,
target_accept=0.9,
random_seed=0,
)
数式で書いた3行が、ほぼそのままの順序でコードに現れています。ベイズモデリングのコードが読みやすいと言われるのは、この対応関係が保たれるためです。
pm.sampleの引数のうち、実務で意味を持つものを補足します。drawsは本番として採用する抽出回数、tuneはウォームアップの回数です。ウォームアップの区間は、サンプラーが自分の刻み幅などを調整するために使われるもので、事後分布の標本としては使わずに捨てます。chainsは独立に走らせる連鎖の本数で、後述する収束診断のために複数本必要です。4本が慣例になっています。target_acceptは受容率の目標値で、これを上げると刻みが細かくなり、難しい形の事後分布でも壊れにくくなります。random_seedは再現性のために必ず固定します。
事後予測は、モデルのブロックを再び開いて実行します。
with model:
idata.extend(pm.sample_posterior_predictive(idata))
# 観測データと事後予測分布の重ね合わせを描画する
az.plot_ppc(idata)
Stanでは、モデルを専用の言語で別ファイルに書きます。ブロックの役割があらかじめ決められているのが特徴で、どこに何を書くかが強制されるぶん、他人の書いたモデルも読みやすくなります。
data {
int<lower=0> N;
vector[N] x;
vector[N] y;
}
parameters {
real alpha;
real beta;
real<lower=0> sigma;
}
model {
// 事前分布
alpha ~ normal(0, 10);
beta ~ normal(0, 10);
sigma ~ normal(0, 10); // lower=0 の制約により半正規分布になる
// 尤度(ベクトル化して書くと速い)
y ~ normal(alpha + beta * x, sigma);
}
generated quantities {
vector[N] y_rep;
for (n in 1:N) {
y_rep[n] = normal_rng(alpha + beta * x[n], sigma);
}
}
dataブロックは外から渡すもの、parametersブロックは推定したいもの、modelブロックは事前分布と尤度、generated quantitiesブロックは推定後に副産物として計算したいものを書きます。sigmaの宣言に付いている<lower=0>が負の値を禁じる制約で、これがあるためにmodelブロックで正規分布を指定しても実質的に半正規分布として働きます。
Pythonから呼ぶ場合はcmdstanpy、Rから呼ぶ場合はcmdstanrを使うのが現在の標準です。Python側のコードは次のようになります。
from cmdstanpy import CmdStanModel
import arviz as az
# .stan ファイルはここでC++にコンパイルされる(初回は時間がかかる)
model = CmdStanModel(stan_file="linreg.stan")
fit = model.sample(
data={"N": len(y), "x": x_std, "y": y},
chains=4,
iter_warmup=1000,
iter_sampling=2000,
adapt_delta=0.9,
seed=0,
)
# 以降の診断は ArviZ に寄せる
idata = az.from_cmdstanpy(fit, posterior_predictive="y_rep")
PyMCのtuneがStanのiter_warmup、drawsがiter_sampling、target_acceptがadapt_deltaに対応します。名前が違うだけで、指しているものは同じです。処理系を移るときは、この対応表を頭に置いておくと迷いません。
両者の書き味の違いも、上の2つを並べれば見えてきます。Stanは変数の型と制約を明示的に宣言させ、コンパイル時に多くの誤りを弾きます。PyMCは宣言を省ける代わりに、誤りが実行時まで表面化しないことがあります。厳密に組むならStan、回転数を上げるならPyMC、という傾向はこの違いから来ています。
サンプリングが終わったら、まず結果を信用してよいかを確かめます。MCMCが返すのはあくまで「事後分布からの標本のつもりの数列」であって、それが本当に事後分布を代表しているかは保証されていないためです。診断は次の順で行います。
import arviz as az
# 数値の要約(平均・標準偏差・信用区間・r_hat・ess_bulk・ess_tail・mcse)
az.summary(idata, var_names=["alpha", "beta", "sigma"])
# トレースプロット(左が事後分布、右が連鎖の軌跡)
az.plot_trace(idata, var_names=["alpha", "beta", "sigma"])
# ランクプロット(連鎖間の偏りを見るのに向く)
az.plot_rank(idata, var_names=["alpha", "beta", "sigma"])
トレースプロットは、横軸に反復回数、縦軸にパラメータの値を取った折れ線です。4本の連鎖が同じ高さの帯に重なり、細かく振動している状態が正常です。俗に毛虫のようだと表現されます。連鎖ごとに違う高さで平行に走っている、全体が右肩上がりに動き続けている、特定の値に張り付いて動かない区間がある、といった様子が見えたら、その結果は使えません。近年はランクプロットも併用されます。全連鎖をまとめて順位に変換し、連鎖ごとの順位のヒストグラムを描くもので、連鎖間のわずかな偏りが目で捉えやすくなります。
\( \hat{R} \)(R-hat、ギリシャ文字の読みからアール・ハットと呼びます)は、連鎖間のばらつきと連鎖内のばらつきの比を取った指標です。すべての連鎖が同じ分布を探索していれば、この比は1に近づきます。判定の目安は1.01未満です。古い教科書には1.1未満と書かれていることがありますが、その基準は緩すぎることが後の研究で分かっており、現在は1.01が実務上の線として使われています。1つでも超えているパラメータがあれば、結果全体を使わないという扱いが安全です。
有効サンプルサイズ(ESS)は、得られた標本が実質何個分の独立な標本に相当するかを表します。MCMCの標本は前後が相関しているため、8,000個抽出しても情報量としては数百個分しかない、ということが普通に起こります。ArviZの要約にはess_bulkとess_tailの2つが出ます。前者は分布の中心付近の推定精度、後者は裾(極端な値の側)の推定精度に対応します。信用区間の端点は裾の情報に依存するため、区間を報告するならess_tailも見る必要があります。目安は、どちらも連鎖1本あたり100、4本なら合計400以上です。
あわせてmcse(モンテカルロ標準誤差)も確認します。これは「計算を打ち切ったことによる誤差」の大きさで、事後分布そのものの幅(標準偏差)に比べて十分小さいことが必要です。報告したい桁数に対してこの誤差が大きいなら、単純に抽出回数を増やせば改善します。ここは、事後分布の幅を狭めることはできない(そちらはデータ量の問題)のに対して、計算を増やせば必ず縮む種類の誤差である点が重要です。
これらの指標はすべて、不合格を検出するための道具です。全部が基準を満たしたからといって、事後分布を正しく捉えた証明にはなりません。探索されないまま残った領域があっても、指標には現れないからです。診断は「落ちたら止める」ための関門として使い、通ったことを積極的な根拠にはしない、という構えが正確です。

実行後に、一定数の遷移が発散したという警告が出ることがあります。これが発散遷移(divergent transition)で、ハミルトニアンモンテカルロ特有の警告です。サンプラーは物理系の運動をシミュレーションする形で次の候補点を探しますが、事後分布の形が急に細くなる領域に入ると、その数値計算が破綻して軌道が飛んでしまいます。破綻した箇所は本来なら探索されるべき領域であることが多く、結果として、そこだけが標本から抜け落ちます。
発散遷移が厄介なのは、抜け落ち方に方向性がある点です。単に精度が落ちるのではなく、推定値が系統的にずれます。しかも\( \hat{R} \)やESSは正常値のままであることが珍しくありません。「他の診断は通っているから数個の警告は無視してよい」という判断は成り立たないと考えてください。
対処は、次の順で試すのが定石です。
target_accept(Stanではadapt_delta)を上げる。0.8が既定なので、0.9、0.95、0.99と刻む。刻み幅が細かくなり計算時間は延びるが、これだけで消えることは多いもう1つよく出る警告に、最大木深度の超過(maximum treedepth exceeded)があります。こちらは性質が違い、正確性ではなく効率の問題です。指定した上限まで探索しても軌道が折り返さなかったという意味で、結果が偏るわけではありません。他の診断指標が良好であれば実害がないことが多いものの、サンプリングの効率が落ちているサインであり、モデルの指定に問題があることを示す場合もあります。計算が無駄に長くなるだけでなく、事後分布が細長くなっていないか(変数のスケールが揃っているか)を確認する契機として扱います。発散遷移は正確性の警告、木深度は効率の警告、と覚えておくと判断を誤りません。
診断を通ったら、事後分布を要約して報告の形にします。ここで実務上のつまずきになりやすいのが、成果物の受け渡し方です。推定結果は、平均値や区間の端点といった数個の数値ではなく、パラメータごとに数千個の乱数が並んだ表として存在しています。この表こそが結論であり、平均や区間はそこから切り出した断片にすぎません。断片だけを次の工程に渡してしまうと、後から別の問いを立てたときに、また推定をやり直すことになります。
そのため、結果はInferenceDataの形式のまま保存しておくのが実務的です。ArviZはaz.to_netcdfとaz.from_netcdfで保存と読み込みができ、処理系を問わず同じファイルを扱えます。これを残しておけば、報告後に「では利益率が5パーセントを超える確率は」と聞かれたときに、保存した標本を数え直すだけで答えが出ます。推定は1回、問いは何度でも、という運用ができます。
import arviz as az
# 推論結果を丸ごと保存する(後から別の問いに答えられるようにするため)
az.to_netcdf(idata, "linreg_posterior.nc")
# 読み込み
idata = az.from_netcdf("linreg_posterior.nc")
報告書に載せる数値をどう選ぶか、区間をどう解釈するかについては第3章で扱った内容がそのまま当てはまります。この章の関心は、その数値を取り出す前段の計算が信用に足るかどうかにあります。順序としては、診断を通す、要約する、報告する、の3段階を崩さないことです。診断を後回しにして先に図表を作ると、あとで結果を差し替えることになり、関係者の間で数値が食い違います。
また、モデルを1つ作って終わりにせず、比較対象を用意しておくと議論が進めやすくなります。説明変数を1つ減らしたモデル、事前分布を変えたモデルなど、少し違う版をいくつか回して、結論がどれだけ動くかを見ます。前提を変えても結論が変わらないなら、その結論は前提に依存していないと言えます。逆に大きく動くなら、どの前提が効いているかを明示したうえで報告する必要があります。ベイズの枠組みは前提を数式として書き下すため、この種の感度の確認が機械的に行える点に利点があります。
最後に、実際に手を動かしたときに引っかかりやすい箇所を挙げます。
初期値。既定では、サンプラーは変換後の空間でランダムな初期値から出発します。多くの場合これで問題ありませんが、制約の強いパラメータや、対数や逆数を含むモデルでは、初期値の時点で対数尤度が計算できず、初期化に失敗することがあります。この場合はinit引数で妥当な値を明示的に与えます。目安として、データから素朴に推定した値(最小二乗法の結果など)を渡すと安定します。
スケール。説明変数の桁が揃っていないと、事後分布が細長く傾いた形になり、サンプラーの効率が大きく落ちます。年齢と年収を同じモデルに入れる、といった場面が典型です。前述のとおり標準化してからモデルに入れ、必要なら推定後に元の単位へ戻します。この1手間で、収束の問題の相当部分が消えます。
非中心化パラメータ化。事後分布の形が原因で発散が起きるとき、数学的に同じモデルを別の書き方に変えると解決することがあります。たとえば\( \theta \sim \mathrm{Normal}(\mu, \tau) \)と書く代わりに、\( z \sim \mathrm{Normal}(0, 1) \)を用意して\( \theta = \mu + \tau z \)と定義します。分布としてはまったく同じですが、サンプラーが動く空間の形が変わり、細く尖った領域が解消されます。データが少なくパラメータ間の依存が強いときほど効果が出ます。書き換えの手間はコード数行なので、発散が消えないときの標準的な一手です。
環境まわり。StanはC++へのコンパイルを伴うため、初回実行時にビルド環境が必要です。Windowsでは特に、対応するツールチェーンが入っているかどうかで最初につまずきます。PyMCとNumPyroはPythonのパッケージ管理だけで完結しますが、こちらはこちらで、数値計算バックエンドとPythonのバージョンの組み合わせに制約があります。分析結果を後から再現する必要がある以上、使用したライブラリのバージョン、乱数種、入力データのハッシュ値は、結果と一緒に記録に残してください。ベイズモデルは確率的な手順で推定するため、条件を記録しなければ厳密な再現ができません。
計算時間との折り合い。データが大きくなるとサンプリングは重くなります。変分推論と呼ばれる近似手法を使えば桁違いに速くなりますが、これは事後分布を扱いやすい形の分布で近似するもので、分布の裾や、パラメータ間の相関を過小評価する傾向があります。探索段階で当たりを付けるには有効でも、最終的な意思決定の根拠として不確実性の幅を報告するなら、MCMCで確認し直すのが安全です。速い手法と正確な手法を、工程の中で使い分けるという発想になります。
ここまでで、モデルを書き、回し、結果を信用してよいかを判定するところまでの道具が揃いました。次章以降では、この道具を使って実際の意思決定に答えを出す場面を扱います。
『StanとRでベイズ統計モデリング』(松浦健太郎、共立出版):Stanによるモデリングの定番書。モデルの書き方だけでなく、収束しないときに何を疑い、どう書き換えるかという実務的な判断がていねいに書かれています。
『Pythonではじめるベイズ機械学習入門』(森賀新・木田悠歩・須山敦志、講談社):PyMCとNumPyroを含む複数の確率的プログラミング言語を、同じ題材で書き比べながら進める構成です。処理系の選定を検討している段階で読むと違いがつかめます。
前章までで、事前分布と尤度から事後分布を得る仕組みと、それをStanやPyMCで実際に走らせる手順を確認しました。本章では、その事後分布を意思決定にそのまま使う場面を扱います。題材はA/Bテストです。
A/Bテストは、統計手法の中でもとりわけ経営判断と距離が近い道具です。ランディングページの新デザイン、メール件名の書き分け、価格表示の並べ方、レコメンド枠の位置。どれも「AとB、どちらを採用するか」という一択に帰着します。にもかかわらず、分析の結果として報告されるのは「有意差が確認できませんでした」という一文であることが少なくありません。決めなければならない場に、決められないという報告が返ってくる。この噛み合わなさは、担当者の力量の問題ではなく、使っている枠組みが答えている問いそのものに由来しています。
本章では、まず仮説検定が答えている問いと意思決定者が知りたい問いのズレを整理し、次にベータ分布と二項分布の組み合わせでA/Bテストをベイズ的に組み立てる手順を示します。そのうえで、途中経過を何度も覗いてよいのかという実務上いちばん問い合わせの多い論点と、ベイズファクターの位置づけを扱います。なお、有意水準や検定統計量といった頻度論側の作法そのものは既に別のコラムで扱っているため、本章では復習に踏み込みません。ここで比べるのは手続きの細部ではなく、視点の置き方です。
p値の定義は明確です。帰無仮説が正しいと仮定したとき、手元のデータと同じかそれ以上に極端な結果が観測される確率を指します。記号で書けば \( P(\text{データ} \mid H_0) \) の形をした量です。仮定を置いて、その仮定のもとでのデータの珍しさを測っています。
一方、A/Bテストの結果を受け取る側が知りたいのは、次のような量です。新デザインAが現行Bより本当に優れている確率はいくらか。優れているとして、どれくらい優れているのか。仮にBのままにした場合、取り逃す成果はどの程度か。これらはいずれも \( P(\text{仮説} \mid \text{データ}) \) の形をしています。データを条件として、仮説の側の確からしさを問うています。
条件の向きが逆になっています。そして第1章で見たとおり、\( P(A \mid B) \) と \( P(B \mid A) \) は一般に別の量です。p値が0.03だったという事実から、「Aが優れている確率は97%」とは言えません。この読み替えは、統計学の講義では最初に戒められる誤りですが、実務の会議では日常的に起きています。誤りだと知っている担当者ほど、正しく言い直そうとして「帰無仮説のもとでこのデータが得られる確率が3%です」と説明し、聞き手の理解が止まるという別の困りごとに突き当たります。
もう一つの噛み合わなさは、検定の出力が二値だという点にあります。検定は棄却するかしないかの二択しか返しません。しかし現実の証拠の強さは連続的です。あと少しで閾値に届かなかった結果と、まったく差の気配がない結果は、意思決定上まるで違う意味を持ちますが、どちらも「有意差なし」に丸められます。そして「有意差なし」は「差がないことが示された」ではありません。棄却できなかったというだけの状態です。差がないのか、データが足りないのかを区別できないまま、報告書には同じ一文が並びます。
さらに、検定の枠組みは事前にサンプルサイズを固定することを前提としています。必要な検出力から必要な観測数を逆算し、その数に達するまでは結果を見ないという運用が正しい作法です。ところが実際のWebサービス運用では、テストは毎日回っており、ダッシュボードは常に開けます。想定より効果が大きければ早く打ち切りたいし、季節要因で流入が読めなければ期間も動きます。運用の実態と手続きの前提が合っていません。
ベイズの枠組みは、これらの噛み合わなさのうちいくつかを構造的に解消します。すべてではありません。何が解消され、何が残るのかを以下で分けて見ていきます。
ベイズ推定の出力は事後分布です。第3章で見たとおり、事後分布が手に入れば、点推定でも区間でも、パラメータについての任意の命題の確率でも、そこから取り出せます。A/Bテストで知りたいのは2つの真のコンバージョン率 \( \theta_A \) と \( \theta_B \) の大小関係ですから、求める量は次のように書けます。
\( P(\theta_A > \theta_B \mid \text{観測データ}) \)
これは新しい検定手法ではありません。事後分布の要約の一種です。平均を取るのと同じ操作で、ただ「差が正である領域の確率質量」を測っているだけです。検定という別枠の道具立てを持ち出す必要がなく、第2章から第5章までで積み上げた仕組みの延長線上にそのまま乗ります。
この量には、報告する側にとって都合のよい性質があります。まず、そのまま日本語になります。「案Aが案Bを上回っている確率は91%です」という文は、統計の訓練を受けていない聞き手にも意味が通り、しかも定義に忠実です。次に、値が連続的です。0.62という値が出たなら「弱い証拠しかない」と正直に言えますし、0.995なら「ほぼ確実」と言えます。閾値の手前か向こうかで表現が断絶しません。
そして、大小関係だけでなく差の大きさも同じ事後分布から取り出せます。\( \theta_A – \theta_B \) の事後分布を作れば、改善幅の中央値も95%信用区間も出ます。相対改善、いわゆるリフトを見たければ \( \theta_A / \theta_B – 1 \) の分布を作ればよく、追加のモデルは要りません。「勝っている確率は高いが、勝ち幅は誤差のようなものだ」という状況を、一つの図の中で示せます。

コンバージョン率のように「見た人のうち何人が成果に至ったか」を扱う場合、モデルの形は第2章で扱った共役の関係がそのまま使えます。観測は二項分布に従うとし、成功確率の事前分布にベータ分布を置く形です。この組み合わせをベータ二項モデルと呼びます。
案Aについて、表示回数を \( n_A \)、成果件数を \( y_A \) とすると、モデルは次のように書けます。
\( y_A \sim \mathrm{Binomial}(n_A, \theta_A) \)、\( \theta_A \sim \mathrm{Beta}(\alpha_0, \beta_0) \)
共役性により、事後分布もまたベータ分布になり、パラメータは次のとおりです。
\( \theta_A \mid y_A \sim \mathrm{Beta}(\alpha_0 + y_A,\ \beta_0 + n_A – y_A) \)
案Bについても同じ形を独立に置きます。MCMCを回す必要すらなく、事後分布は閉じた式で書けます。第4章で扱ったサンプリングの機構は、この単純な例では出番がありません。逆に言えば、A/Bテストのベイズ化は計算量の面でほとんど障壁がないということでもあります。
実務上の判断が入るのは事前分布の置き方です。よく使われる選択肢を整理します。
| 事前分布 | 性格 | 使いどころ |
|---|---|---|
| \( \mathrm{Beta}(1, 1) \) | 0から1まで一様。何も知らない状態に近い | まったく新しい指標で、水準の見当がつかないとき |
| \( \mathrm{Beta}(0.5, 0.5) \) | ジェフリーズ事前。端の値に少し重みが残る | 成果件数が極端に少ない場面で、無情報に近く保ちたいとき |
| \( \mathrm{Beta}(3, 97) \) | 3%前後を中心とする弱情報事前。疑似的な観測100件ぶんの重み | 過去の実績からおおよその水準が分かっているとき |
ベータ分布の2つのパラメータは、\( \alpha_0 \) を「過去に見た成功数」、\( \beta_0 \) を「過去に見た失敗数」と読めます。したがって \( \alpha_0 + \beta_0 \) が事前分布の効き目の強さ、つまり疑似的なサンプルサイズになります。実データが1万件あるときに \( \alpha_0 + \beta_0 = 100 \) の事前を置いても、結論はほとんど動きません。事前分布を入れることは結論を歪める行為ではなく、データが少ない初期段階での暴れを抑える保険として働きます。
事前分布の選択が結論を左右するかどうかは、実際に複数の事前で計算して確かめるのが確実です。\( \mathrm{Beta}(1,1) \) と弱情報事前で \( P(\theta_A > \theta_B) \) が大きく変わるなら、それはデータが少なすぎるという警告として読むべきで、都合のよい事前を選ぶ理由にはなりません。
計算はモンテカルロで十分です。両方の事後分布から乱数を大量に引き、Aのほうが大きかった割合を数えます。
import numpy as np
rng = np.random.default_rng(0)
# 観測データ(表示回数と成果件数)
n_a, y_a = 12000, 360 # 案A
n_b, y_b = 12000, 318 # 案B
# 事前分布 Beta(alpha0, beta0):過去実績の3%前後を弱く反映させる
alpha0, beta0 = 3.0, 97.0
# 共役性により事後分布もベータ分布になる
post_a = rng.beta(alpha0 + y_a, beta0 + n_a - y_a, size=200000)
post_b = rng.beta(alpha0 + y_b, beta0 + n_b - y_b, size=200000)
# 「AがBに勝っている確率」は、単に勝った回数の割合
p_a_wins = (post_a > post_b).mean()
# 差と相対改善(リフト)も同じサンプルから取り出せる
diff = post_a - post_b
lift = post_a / post_b - 1.0
print("P(A > B) =", p_a_wins)
print("CVR差の中央値 =", np.median(diff))
print("相対改善の95%信用区間 =", np.quantile(lift, [0.025, 0.975]))
20万回の乱数生成は一瞬で終わります。ここで得られる p_a_wins がそのまま報告できる数字であり、lift の分位点が改善幅の不確実性を表します。ダッシュボードに組み込む場合も、この数行を日次バッチで回すだけで済みます。
「AがBに勝つ確率は91%」という報告は分かりやすい一方、それだけでは判断の材料として足りません。勝つ確率が91%でも、勝ち幅が実務上ほぼゼロなら、切り替える手間に見合いません。逆に勝つ確率が70%しかなくても、勝つときの上振れが大きく、負けるときの下振れが小さいなら、採用する価値があります。確率と大きさを一つの数字にまとめる指標が期待損失です。
案Aを選んだときの期待損失は、次のように定義します。
\( \mathrm{Loss}(A) = E\big[\max(\theta_B – \theta_A,\ 0)\big] \)
意味は素直です。Aを選んだ結果、実はBのほうが良かったという場合にだけ損失が発生し、その大きさは差そのものです。Aのほうが良かった場合の損失は0とします。この期待値を事後分布の上で取ります。案Bについても同様に計算します。
期待損失は「その案を選んだとき、取り逃すコンバージョン率の期待値」という単位を持ちます。単位を持つということは、実務的な閾値と直接比べられるということです。たとえば「1万セッションあたり2件ぶんの差なら、運用コストを考えれば無視してよい」という合意が社内にあるなら、期待損失がその水準を下回った時点で判断を確定できます。この「差があるとは言わないことにする幅」を、実務的同等域と呼ぶことがあります。ベイズの枠組みでは、事後分布のうちこの同等域に入る確率を直接計算できるため、「差はあるが実務的には同じ」という結論を積極的に述べられます。検定では、この結論を述べる手段がありません。
先ほどのコードに数行を足すだけで求まります。
# それぞれの案を採用したときに「取り逃すCVR」の期待値
loss_a = np.maximum(post_b - post_a, 0.0).mean()
loss_b = np.maximum(post_a - post_b, 0.0).mean()
# 実務的同等域(ここでは±0.1ポイント)に差が収まっている確率
rope = ((diff > -0.001) & (diff < 0.001)).mean()
print("期待損失: A =", loss_a, "/ B =", loss_b)
print("差が実務的同等域に収まっている確率 =", rope)
運用ルールの形にすると、「期待損失が閾値を下回った案を採用する。どちらも下回らないうちは継続する」となります。これは「有意差が出るまで待つ」よりも意思決定に直結しています。差が本当に小さい場合、検定はいつまでも有意になりませんが、期待損失は両案ともに小さくなり、「どちらを選んでも取り逃す量はこの程度なので、実装が容易なほうにする」という結論に到達できます。煮え切らなさから抜けられるのは、この経路によります。
切り替えに固定的なコストがかかる場合は、その分を明示的に織り込みます。現行Bから新案Aへの移行に開発工数がかかるなら、Aを採る条件は「Aの期待利得が移行コストを上回ること」です。事後分布から期待利得の分布が出ているので、金額換算した比較がそのまま書けます。統計の結論と事業の結論を別々の言語で語らずに済む点は、経営層への説明において実利があります。
実務でもっとも頻繁に問われるのがこの点です。頻度論の枠組みでは、有意になるまで結果を覗き続けて任意の時点で打ち切る運用は、第一種の過誤を大きく膨らませます。真の差がゼロであっても、覗く回数を増やせばいつかは閾値を跨ぐためです。これを避けるために逐次検定や事前のサンプルサイズ固定といった手続きが必要になります。
ベイズの事後分布については、事情が異なります。事後分布は「いま手元にあるデータのもとでの信念」を表す量であり、その定義はデータをいつ見たかに依存しません。10日目に計算しても20日目に計算しても、そのときのデータに対する事後分布は定義どおり正しい値です。観測を止めた理由が観測の内容と無関係な情報に基づく限り、尤度の形が変わらないため、事後分布も変わりません。この性質は尤度原理と呼ばれる考え方に支えられています。尤度原理とは、データが推論に対して持つ情報は尤度の形にすべて含まれており、そのデータをどのような手順で集めて、どこで観測を止めたかは推論の内容を変えない、という立場を指します。頻度論の多重比較のような、覗いた回数ぶんだけ補正するという操作は要りません。
ただし、これを「何をしてもよい」と読むと誤ります。誠実に言えば、無条件ではありません。問題は事後分布そのものではなく、停止規則にあります。
「\( P(\theta_A > \theta_B) \) が0.95を超えた瞬間に止める」という規則を置くと、止まる時点は偶然の揺れが大きい側に偏って選ばれます。サンプルが少ない段階では事後分布の幅が広く、確率は日々大きく動きます。その動きの上端を捉えたところで打ち切っているのですから、採用した案の効果量は真の値より大きく見積もられます。これは事後分布の誤りではなく、「事後確率がある値を超えた時点だけを切り出す」という運用が持つ選択バイアスです。オプショナルストッピングと呼ばれるこの論点は、ベイズだからといって消えるわけではありません。事後分布は正しいまま、運用ルールとしての誤り率と効果量の推定精度が影響を受けます。
実務では次の対処を組み合わせます。
最後の項目は、手間の割に得るものが大きい作業です。自社の運用ルールが実際にどの程度の誤りを出すのかを、公式ではなく実測で把握できます。
import numpy as np
def run_one_test(theta_a, theta_b, rng, n_max=20000, check_every=500,
n_min=2000, threshold=0.95, alpha0=3.0, beta0=97.0,
n_draw=20000):
"""指定した停止規則で1回ぶんのA/Bテストを走らせ、打ち切り時点と判定を返す"""
a_seq = rng.random(n_max) < theta_a
b_seq = rng.random(n_max) < theta_b
for n in range(check_every, n_max + 1, check_every):
if n < n_min:
continue
ya, yb = a_seq[:n].sum(), b_seq[:n].sum()
pa = rng.beta(alpha0 + ya, beta0 + n - ya, n_draw)
pb = rng.beta(alpha0 + yb, beta0 + n - yb, n_draw)
p = (pa > pb).mean()
if p > threshold or p < 1.0 - threshold:
return n, p
return n_max, p
# 真の差がゼロの世界で運用ルールを繰り返し、誤って勝敗を宣言する割合を測る
rng = np.random.default_rng(0)
results = [run_one_test(0.03, 0.03, rng) for _ in range(1000)]
false_calls = np.mean([p > 0.95 or p < 0.05 for _, p in results])
print("真の差がゼロのときに勝敗を宣言した割合 =", false_calls)
n_min や threshold を変えて同じ実験を回せば、自社のルールがどこで安定し、どこで崩れるかが分かります。真の差を0.03と0.033のように小さく設定して実行すれば、必要な観測数の見当も付きます。検出力の公式を暗記する代わりに、実際の運用条件そのものを試せる点が、この方法の利点です。

ベイズの枠組みで検定に対応する道具としてしばしば紹介されるのがベイズファクターです。2つのモデル、たとえば「差がない」モデル \( M_0 \) と「差がある」モデル \( M_1 \) について、それぞれのデータの生じやすさの比を取ります。
\( \mathrm{BF}_{10} = \dfrac{p(D \mid M_1)}{p(D \mid M_0)} \)
ここで \( p(D \mid M) \) は周辺尤度、すなわちモデル内のパラメータについて積分して得られるデータの確率です。事前オッズにベイズファクターを掛けると事後オッズになるという関係が成り立つため、「このデータは、どちらのモデルをどれだけ支持したか」という証拠の強さを表す量として読めます。
利点は明確です。差がないという主張を積極的に支持できます。検定では帰無仮説を採択できませんが、ベイズファクターは1を下回ることで \( M_0 \) 側への支持を示せます。医学や心理学の領域で「効果がないことを示したい」場面に使われるのはこのためです。
一方で、実務のA/Bテストで主役に据えるには扱いにくい面があります。第一に、事前分布への感度が高い量です。とくに「差がある」モデルの側で効果量にどれだけ広い事前を置くかによって値が大きく動きます。差がないモデルを1点に絞ったうえで対立側の事前を極端に広げると、データが何であれ差がないモデルが支持されやすくなるという現象が知られています。第二に、周辺尤度の計算が重く、モデルが複雑になると数値的に不安定になります。第三に、値の大きさを「強い証拠」「決定的な証拠」と読み替える目安表が広く使われていますが、その区切りは慣習であって、意思決定の損得とは無関係です。
整理すると、問いの性質で使い分けるのが素直です。「効果があるモデルとないモデルの、どちらがデータをよく説明するか」というモデル選択の問いにはベイズファクターが向きます。「AとB、どちらを採用し、間違えたときにいくら失うか」という意思決定の問いには、事後確率と期待損失のほうが直接的です。ビジネスのA/Bテストで問われるのはほぼ後者ですから、本章では後者を軸に置いています。ベイズファクターは知識として押さえておき、必要になった場面で持ち出すという位置づけで足ります。
ベイズでA/Bテストを回す場合、報告の型をあらかじめ決めておくと運用が安定します。次の項目を並べれば、読み手が判断に必要な情報が揃います。
7番目を先に決めて文書に残しておくことは、ベイズであっても重要です。事後分布はいつ見ても正しいものの、都合のよい時点を後から選んで報告すれば、読み手に伝わる情報は歪みます。停止規則と閾値を事前に書き、そのとおりに運用した記録を添える。この習慣は、頻度論の事前登録と同じ意図を持ちます。
3案以上を比較する場合も、枠組みはそのまま拡張できます。各案の事後分布からサンプルを引き、「その案が最良である回数の割合」を数えれば、案ごとの最良確率が出ます。さらに、この最良確率に応じて配信量を動的に配分すれば、テスト中の機会損失を減らす運用になります。複数の選択肢を順に試しながら、成績のよいものへ配分を寄せていくこの種の問題は、多腕バンディットと呼ばれます。名称は変わりますが、出発点は本章と同じ事後分布です。
適用範囲についても、正直に線を引いておきます。ベータ二項モデルが使えるのは、成果が0か1かで表される指標に限られます。1人あたりの購入金額のように裾が重く、ゼロが多く混じる指標では、二項分布は適合しません。この場合は対数正規分布やゼロ過剰モデルを使うことになり、共役の関係は失われるので、第5章で扱ったStanやPyMCの出番になります。指標の性質を確かめずにコンバージョン率と同じコードを流用すると、事後分布そのものが誤ります。
そして最後に、モデルの外側にある前提を確認しておく必要があります。事後確率は「モデルが正しく、割当がランダムで、計測が正確ならば」という条件付きの量です。トラフィックの割当に偏りがあれば、あるいは計測タグの実装に漏れがあれば、どれほど洗練された事後分布を計算しても結論は誤ります。ベイズは不確実性の表現を改善しますが、データの取り方の欠陥を補正する仕組みは持っていません。この点は頻度論の検定とまったく同じです。手法の選択より前に、実験設計と計測の健全性を確認する工程が要ります。
本章で確認したのは、A/Bテストの結論を「有意差の有無」ではなく「勝つ確率」と「取り逃す期待値」で述べる方法でした。次章では、複数の店舗や地域といった単位が並ぶ状況で、データの少ない単位の推定をどう安定させるかという問題に移ります。本章で扱った案ごとの独立したモデルを、単位間で情報を共有する構造に置き換える話になります。
『ウェブ最適化ではじめる機械学習 A/Bテスト、バンディットアルゴリズム、ベイズ最適化、統計モデリング』(飯塚修平、オライリー・ジャパン):ベータ二項モデルによるA/Bテストから多腕バンディット、ベイズ最適化までを、Webサービスの改善という一貫した文脈で追える一冊です。本章で扱ったP(A>B)の考え方を、実装まで含めて広げたい場合に適しています。
『ベイズ統計モデリング:R,JAGS,Stanによるチュートリアル 原著第2版』(John K. Kruschke著、前田和寛・小杉考司監訳、共立出版):検定のベイズ的代替を正面から扱った教科書です。本章で触れた実務的同等域の考え方や、事後分布に基づく判断の枠組みが詳しく解説されており、頻度論の検定に慣れた読者が発想を切り替える助けになります。
実務のデータは、たいてい何らかのグループに分かれています。店舗別の売上、営業所別の受注率、製品カテゴリ別の返品率、地域別の発生件数、担当者別の成約率。分析の依頼も「全社の平均ではなく、店舗ごとの実力を知りたい」という形で来ることが多く、グループ単位の数字を出すこと自体が目的になります。
ところが、グループに分けた瞬間にデータは急に少なくなります。全社で1万件あった取引も、200店舗に分ければ1店舗あたり平均50件です。しかも均等には分かれません。大型店には500件あり、開店したばかりの店には3件しかない、という偏りが普通に生じます。3件のうち1件が成約なら成約率は33パーセントですが、この33パーセントを全社ランキングの上位として扱ってよいかというと、明らかに無理があります。
この問題は、単に「サンプルが少ないから信頼できない」という一言で片づくものではありません。厄介なのは、少数データのグループほど極端な値を取りやすいという構造的な偏りが生じる点です。分母が3のときに取り得る成約率は0パーセント、33パーセント、67パーセント、100パーセントの4通りしかなく、中間の値が存在しません。分母が500のグループは全社平均の近くに落ち着きますから、ランキングの上位と下位は自動的に小規模グループで占められます。「売上トップの店舗」と「売上ワーストの店舗」が両方とも小さな店だった、という現象は偶然ではなく、この構造から必然的に出てきます。
この章で扱う階層ベイズモデルは、この状況を正面から扱うための道具です。データの少ないグループの推定値を、全体の傾向に向けて適度に引き寄せることで、極端な値に振り回されない推定を行います。しかも引き寄せる強さを分析者が恣意的に決めるのではなく、データそのものから決めます。
グループのあるデータを扱うとき、選択肢は原理的に三つしかありません。
一つめは完全プーリングです。グループの区別を無視して、全データをひとまとめにして一つの値を推定します。先ほどの例なら、全社の成約率だけを出して、どの店舗にもその値を当てはめます。データを最大限に使うので推定は安定しますが、店舗ごとの違いは一切表現できません。「店舗ごとの実力を知りたい」という当初の目的には答えられていません。
二つめは完全分離です。グループごとに独立にモデルを当てはめます。3件しかない店舗は3件だけで推定し、500件ある店舗は500件だけで推定します。グループの違いは最大限に表現されますが、少数データのグループの推定値は不安定になり、先ほどの極端値の問題がそのまま出ます。さらに、まだ1件もデータのない新規店舗には何も言えません。
三つめが部分プーリングです。グループごとに別々の値を持つことは認めつつ、それらの値が「共通の分布から生まれてきた」と仮定します。この仮定を置くと、あるグループの推定に他のグループの情報が間接的に使えるようになります。データの多いグループは自分のデータで推定がほぼ決まり、データの少ないグループは全体の分布に引き寄せられる。この使い分けが自動的に起きるのが部分プーリングの性質です。
| 方針 | グループ差の表現 | 少数グループの安定性 | 使えるデータ量 |
|---|---|---|---|
| 完全プーリング | 表現できない | 安定(ただし全部同じ値) | 全データ |
| 完全分離 | 完全に表現 | 非常に不安定 | そのグループのみ |
| 部分プーリング | 表現できる | 安定 | そのグループ+全体の分布 |
完全プーリングと完全分離は、部分プーリングの両極端な特殊ケースとして位置づけられます。グループ間のばらつきをゼロと仮定すれば完全プーリングになり、無限大と仮定すれば完全分離になります。階層ベイズモデルは、この両極端の間のどこに落とすかを、データから推定します。
形を具体的に書きます。グループ \( g \) に属する \( i \) 番目の観測値を \( y_{g,i} \) と書き、グループ \( g \) の真の水準を \( \theta_g \) とします。
\( y_{g,i} \sim N(\theta_g, \sigma^2) \)
\( \theta_g \sim N(\mu, \tau^2) \)
\( \mu \sim N(0, 100^2) \)、\( \tau \sim \mathrm{Half\mathchar`-Cauchy}(0, 5) \)
一行目は観測モデルです。各グループの観測値は、そのグループの水準 \( \theta_g \) を中心にばらつくとしています。ここまでは完全分離のモデルと同じです。
二行目が階層ベイズモデルの本体です。グループごとの水準 \( \theta_g \) 自体を、平均 \( \mu \)、分散 \( \tau^2 \) の正規分布から生まれた確率変数として扱っています。第2章で見た事前分布は分析者が外から与えるものでしたが、ここでは事前分布のパラメータ \( \mu \) と \( \tau \) 自身が未知数として推定対象に入っています。このように、事前分布のパラメータをさらに推定する対象にしたものを超母数(ハイパーパラメータ)と呼び、超母数に置く分布を超事前分布と呼びます。三行目がそれにあたります。
階層と呼ぶのは、この入れ子構造のためです。観測値の層、グループ水準の層、全体分布の層という三段構えになっていて、情報が層をまたいで行き来します。あるグループのデータは、まず自分の \( \theta_g \) を押し上げ、その \( \theta_g \) が全体分布の \( \mu \) と \( \tau \) の推定に寄与し、更新された \( \mu \) と \( \tau \) が他のグループの \( \theta \) の推定に影響します。データが少ないグループにとっては、この経路が「全体から情報を借りる」通路になります。
ここで重要なのは、借りているのは他のグループの生データそのものではなく、他のグループから推定された「グループ水準がどのくらいばらつくか」という情報だという点です。店舗Aの推定に店舗Bの売上が直接足されるわけではありません。全店舗の実績から「店舗間の実力差はおおよそこの程度に収まる」という分布が推定され、その分布が店舗Aの事前分布として働きます。この間接性が、部分プーリングを恣意的な平均化と分けている点です。
階層モデルを当てはめると、各グループの推定値は完全分離の推定値よりも全体平均に近い位置に落ち着きます。この現象を縮約(shrinkage)と呼びます。なぜそうなるのかは、正規分布の場合には式で確かめられます。
グループ \( g \) の観測数を \( n_g \)、そのグループの標本平均を \( \bar{y}_g \) とすると、\( \mu \) と \( \sigma^2 \)、\( \tau^2 \) が既知であるとき、\( \theta_g \) の事後平均は次の形になります。
\( \hat{\theta}_g = w_g \bar{y}_g + (1 - w_g) \mu \)
\( w_g = \dfrac{n_g / \sigma^2}{n_g / \sigma^2 + 1 / \tau^2} \)
推定値は、そのグループの標本平均と全体平均の加重平均になっています。重み \( w_g \) の分子はそのグループのデータが持つ情報量で、観測数 \( n_g \) が増えるほど大きくなります。分母に加わっている \( 1/\tau^2 \) は、全体分布が持つ情報量です。
この式から性質が読み取れます。観測数 \( n_g \) が大きいグループでは \( w_g \) が1に近づき、推定値はほぼ標本平均そのものになります。観測数が小さいグループでは \( w_g \) が小さくなり、推定値は全体平均 \( \mu \) の側に強く引き寄せられます。極端に \( n_g = 0 \) なら \( w_g = 0 \) となり、推定値は全体平均そのものです。データのない新規店舗に対して「全社平均と同じ」と答えるのは、直感的にも妥当な振る舞いです。
つまり縮約は、分析者が「小さい店は信用できないから平均に寄せておこう」と手心を加えた結果ではありません。ベイズの定理を階層構造に適用した結果として自動的に出てくる帰結です。引き寄せの強さは観測数とばらつきの比で決まり、グループごとに異なる値になります。

重み \( w_g \) には \( \sigma^2 \) と \( \tau^2 \) の二つの分散が入っています。この二つは意味が違います。
\( \sigma^2 \) はグループ内のばらつきです。同じ店舗の中で、日ごと案件ごとに結果がどれだけ揺れるかを表します。この値が大きいということは、観測されたグループ平均が偶然に左右されやすいということですから、縮約は強くなります。
\( \tau^2 \) はグループ間のばらつきです。店舗ごとの実力差が本当にどれだけあるかを表します。この値が大きい、つまり店舗間に本物の差が大きいと判断されれば、各店舗の観測値を素直に信じてよいことになり、縮約は弱くなります。逆に \( \tau^2 \) が小さいと推定されれば、店舗間に差はほとんどないという判断ですから、全店舗の推定値は全体平均の周りに集まります。
この \( \tau \) をデータから推定する点が、階層ベイズモデルの核心にあたります。分析者は「どのくらい平均に寄せるか」を決めません。決めるのはデータです。店舗間の差が実在するデータであれば \( \tau \) は大きく推定されて差が残り、差がほとんどないデータであれば \( \tau \) は小さく推定されて差は消えます。
ただし \( \tau \) の推定には注意が必要です。グループ数が少ないと \( \tau \) 自体の推定が不安定になり、事後分布がゼロ付近に張り付いて全グループが同じ値に潰れることがあります。グループ数が5や6しかない場合は、\( \tau \) の事前分布の選び方が結果に効いてきます。半コーシー分布や半正規分布のような、ゼロを許しつつ裾を持つ分布が推奨されるのはこのためです。また第4章で扱ったMCMCの実行上の問題として、階層モデルは事後分布が細長い漏斗のような形になりやすく、サンプラーが探索に苦労することがあります。この場合は変数を標準正規分布からの変換として書き直す非中心化パラメータ化が有効です。
縮約は、機械学習で言う正則化とほぼ同じ働きをしています。完全分離のモデルは、グループの数だけパラメータを持ちますから、グループ数が多くデータが少ない状況では容易に過学習します。観測されたグループ平均を完璧に再現しますが、その多くは偶然の産物であり、翌期のデータでは再現されません。
階層モデルは、グループごとのパラメータに「共通分布から生まれる」という制約を課すことで、パラメータが自由に動ける範囲を狭めています。これは正則化項でパラメータの大きさに罰則を与えるのと同じ効果です。実際、正規分布の階層事前分布はリッジ回帰の罰則項に対応し、ラプラス分布の階層事前分布はLasso回帰に対応することが知られています。
違いは罰則の強さの決め方にあります。リッジ回帰では正則化の強さを交差検証で外から選びますが、階層ベイズモデルでは \( \tau \) の推定を通じてモデルの内部で決まります。さらに、正則化の強さ自体の不確実性も事後分布として得られますから、「どの程度縮約すべきか自信がない」という状態も表現できます。
この観点に立つと、階層モデルは予測性能の面でも有利になります。過去のデータへの当てはまりは完全分離のモデルの方が良くなりますが、将来のデータに対する予測誤差は階層モデルの方が小さくなることが多くあります。第3章で触れた情報量規準を使ってこの差を確認できます。実務では「昨年のランキングは的中したが今年は全く当たらない」という状況が起きがちで、その多くは完全分離の推定を使っていることが原因です。
ここからは実データでの例を見ていきます。題材は地域別のデータです。市区町村や都道府県といった地域は、まさに「グループが多く、グループごとのデータが少ない」典型で、階層ベイズモデルの効果が見えやすい対象です。
データはe-Statから取得します。e-Statは、日本政府が調査した統計データを閲覧・ダウンロードできるように管理されたポータルサイトで、国勢調査、経済統計、人口動態、労働統計、産業統計など、日本政府機関や関連組織によって収集された様々な統計情報を一括して提供しています。
e-Statは一般市民や研究者、企業など、誰でも無料でアクセスできるオープンなプラットフォームです。ユーザー登録もメールアドレス・所属などを入力するだけで済み、登録してログインすると、アプリケーションIDを取得するページに行くことができます。APIを自分が公開しているWebサイトから利用したい場合は、そのWebサイトのURLを入力します。試験的な利用目的だけであればhttp://localhost/を入力しておけば問題ありません。「発行」を押すと「appId」にアプリケーションIDが発行され、これをリクエストパラメータで渡すことでデータを取得できるようになります。
可視化にはfoliumを使います。OpenStreetMapをJavaScriptで表示するライブラリとしてLeaflet.jsというものがありますが、foliumはこれをPython上で動作させるものです。geojsonやtopojsonを読み込むことができますので、日本の都道府県データのgeojsonとe-Stat APIから取得して加工したデータフレームを紐付けて、Leaflet上に可視化することができます。
folium: https://github.com/python-visualization/folium
まず、階層ベイズに入る前の準備として、e-Stat APIから国勢調査の人口データを取得してfoliumで塗り分け地図にするコードを示します。
先にお断りしておくと、本章に載せたコードと実行結果は、e-Stat APIのバージョン2.1、当時のfolium、PyMC3で実装したときの実測です。現行の環境で動かす場合は、次のように読み替えてください。e-Stat APIは3.0系が現行で、エンドポイントはhttps://api.e-stat.go.jp/rest/3.0/app/になります。地図の塗り分けはfolium.Choropleth(...).add_to(m)を使います。Map.choropleth()とStamen系のタイル指定は現行のfoliumでは利用できません。PyMCはimport pymc as pmが基本の形になり、後半で扱うCAR事前分布はpm.CAR(mu=..., W=adj_matrix, alpha=..., tau=...)として用意されています。距離行列と隣接行列を重み付きで足して共分散行列を組み立てpm.MvNormalに渡す書き方は、行列が正定値になる保証がないため、現行環境ではpm.CARに置き換える方が安全です。地区別のパラメータについても、数式のとおり地区ごとの値を持たせるにはshapeまたはdimsで地区数ぶんの長さを明示してください。
import numpy as np
import pandas as pd
import urllib.request
import folium
from IPython.display import display
appid = "YOUR APPLICATION ID"
api_version = "2.1"
base_url = "http://api.e-stat.go.jp/rest/{api_version}/app/".format(api_version=api_version)
"""
get_type = "getStatsList"
stats_code = "00200521" # 国勢調査
url = base_url + "{get_type}?appId={appid}&statsCode={stats_code}".format(
api_version=api_version,
get_type=get_type,
appid=appid,
stats_code=stats_code,
)
print(url) # 確認して取得したいデータのIDを調べる
"""
"""
get_type="getStatsData"
stats_data_id="0003148596" # 最新の調査ID
url = base_url + "{get_type}?appId={appid}&statsDataId={stats_data_id}".format(
api_version=api_version,
get_type=get_type,
appid=appid,
stats_data_id=stats_data_id
)
print(url) # 確認して取得したい項目パラメータを調べる
"""
# load data from e-stat api
get_type="getSimpleStatsData"
stats_data_id="0003148596"
cd_cat_01="0000" # 国籍->全て
cd_cat_02="0000" # 性別->男女
cd_cat_03="00710" # 集計地域->全域
lv_area="2" # 集計レベル->都道府県レベル
section_header_flg="2" # セクションヘッダー->無し
url = base_url + "{get_type}?appId={appid}&statsDataId={stats_data_id}&cdCat01={cd_cat_01}&cdCat02={cd_cat_02}&cdCat03={cd_cat_03}&lvArea={lv_area}§ionHeaderFlg={section_header_flg}".format(
api_version=api_version,
get_type=get_type,
appid=appid,
stats_data_id=stats_data_id,
cd_cat_01=cd_cat_01,
cd_cat_02=cd_cat_02,
cd_cat_03=cd_cat_03,
lv_area=lv_area,
section_header_flg=section_header_flg,
)
d = urllib.request.urlopen(url).read().decode("utf8")
dlines = d.splitlines()[2:]
jcodes = []
names = []
populations = []
for line in dlines:
line2 = line.replace('"', "").split(",")
jcode = line2[8]
name = line2[9]
population = line2[13]
jcode = jcode[0:2]
population = int(population)
jcodes.append(jcode)
names.append(name)
populations.append(population)
df = pd.DataFrame({"jcode" : jcodes, "name" : names, "population" : populations})
display(df)
# create leaflet map by folium
location = [39.702053, 141.15448379999998]
tiles="Stamen Toner"
zoom_start = 5
map = folium.Map(location=location, tiles=tiles, zoom_start=zoom_start)
map.choropleth(
geo_path="japan.geojson",
data=df,
columns=["jcode", "population"],
key_on="properties.JCODE",
threshold_scale=[10000, 50000, 100000, 150000, 200000, 300000],
fill_color="YlGnBu", fill_opacity=0.7, line_opacity=0.2)
display(map)

e-Stat公式ページの「提供データ」によれば、統計表情報取得「getStatsList」で統計表情報が取得でき、パラメータの政府統計コードは、例えば「国勢調査」は上記コードの通り「00200521」です。これをリクエストパラメータに設定してアクセスすると、これまでに調査された国勢調査のデータ一覧が確認できます。取得したいデータのIDを確認して、統計データ取得「getStatsData」でアクセスすると、項目などの情報とデータが取得できます。この例では、項目の絞り込みなどのパラメータも確認したうえで「getSimpleStatsData」でCSVとして取得し、データフレーム化する形にしました。
ここから本題である、階層ベイズモデルによる地域別データ分析に入ります。推定対象は「地域別の自殺リスク」です。地域別の自殺数を用いて普通にリスクを算出しようとすると、地域によっては値が小さすぎて適切な値が得られないという問題があります。これはこの章の前半で扱った「グループごとのデータが少ない」問題そのものです。この例では、ベイズモデルで空間的な相関を導入することでこれを解決します。
まずリスクの定義を確認します。疫学の分野では、ある基準集団と比べて相対的にリスクが高いか低いかを表す指標として標準化死亡比(SMR)が使われます。ある地域が基準集団と同じリスクを持つ場合の死亡数を期待死亡数と呼び、次のように計算します。
期待死亡数: \( y = \sum \left( n_i \frac{Z_i}{N_i} \right) \)
\( Z_i \): 基準集団での年齢の死亡数
\( N_i \): 基準集団の年齢の人口
\( n_i \): その地域の年齢の人口
観測された死亡数がこの期待死亡数に比べて多い場合、死亡のリスクが大きいと考えられます。この比が標準化死亡比です。
\( SMR = \frac{z}{y} \)
\( z \): 観測死亡数
年齢構成を揃えたうえで比較するための指標である点が重要です。高齢者の多い地域は死亡数が多くなるのが当たり前ですから、単純な人口あたりの件数では地域を比較できません。期待死亡数は「全国と同じ年齢別リスクを持つと仮定したときに、この地域の年齢構成なら何件になるはずか」を計算しています。観測値をこの期待値で割ることで、年齢構成の違いを取り除いた相対リスクが得られます。
この比率をデータから単純に計算することもできますが、ここではこれを推定する方向で実装します。単純計算では、人口の少ない県で1件の増減が比率を大きく動かしてしまうためです。
SMRを地域の相関が考慮されるように推定する階層ベイズモデルを次のように設計します。
\( z_i \sim Poisson(\lambda_i) \)
\( \lambda_i = y_i \exp(\phi_i + \psi_i) \)
\( \phi_i \sim N(0, \sigma^2) \)
\( \psi_i | \psi_{j,i} \sim N\left( \frac{1}{m_i} \sum_{j \in n(i)} \psi_j, \frac{1}{m_i} \sigma_v^2 \right) \)
\( z_i \): 地区iの死亡数
\( y_i \): 地区iの期待死亡数
\( \phi_i \): 地区固有の効果を表すパラメータ
\( \psi_i \): 地区iは隣接地区と似通った傾向を持つことを表すパラメータ
データは死亡数という計数値なのでポアソン対数正規モデルとし、ポアソン分布のパラメータに地域相関のあるSMRがかかる形にしています。ここで \( \exp(\phi_i + \psi_i) \) が地区 \( i \) のSMRの推定値に相当します。
この式の読み方を補足します。\( \lambda_i = y_i \exp(\phi_i + \psi_i) \) は、期待死亡数 \( y_i \) に倍率 \( \exp(\phi_i + \psi_i) \) をかけた値が、実際の死亡数の平均になるという意味です。倍率が1なら全国並み、1を超えればリスクが高い地域ということになります。指数を取っているのは倍率を必ず正の値にするためで、加法的な効果を掛け算の世界に移す標準的な書き方です。
\( \phi_i \) と \( \psi_i \) の役割の違いが、このモデルの階層構造にあたります。\( \phi_i \) は平均ゼロの正規分布に従う地区固有の揺らぎで、この章の前半で見た標準的な部分プーリングの成分です。全国平均へ向かう縮約はここから生じます。
\( \psi_i \) の方は、隣接する地区の値の平均を中心とする正規分布に従います。\( n(i) \) は地区 \( i \) に隣接する地区の集合、\( m_i \) はその数です。隣が高ければ自分も高いと推定される方向に働きますから、地理的に近い地域は似た値を取るようになります。この形の事前分布は、条件付き自己回帰モデル(Conditional Autoregressive Model)にもとづくもので、頭文字を取ってCAR事前分布と呼ばれます。分散が \( \sigma_v^2 / m_i \) となっている点にも意味があり、隣接地区の数が多い地区ほど分散が小さく、周囲の情報が強く効きます。
ここでの「借りる」相手が全体だけでなく隣接地域でもある点が、地理データ特有の拡張です。データの少ない県は、全国平均からも情報を借りますが、それ以上に隣県から情報を借ります。グループ間に「近さ」の構造があるとき、その構造を事前分布に書き込めるのは階層ベイズモデルの柔軟な点です。同じ考え方は、店舗を商圏タイプでまとめる、製品を上位カテゴリでまとめる、といった形で他の対象にも移せます。
上記の統計モデルをPythonで定義します。この例ではPyMCを使います。PyMCにはPyMC2系列とPyMC3系列があり、PyMC3の方を使います。PyMCはコードがスマートになりやすく、モデルの構造も直感的に分かりやすい反面、ネット上の情報はStanの方が多いという違いがあります。
PyMC3: https://github.com/pymc-devs/pymc3
データはe-Stat APIから直接取得し、ベイズ推定はPyMC3で行い、推定結果をfoliumでマップ上に可視化します。都道府県別の自殺数、都道府県別かつ年齢階級別の人口、全国の年齢階級別の自殺数、全国の年齢階級別の人口という4種類の統計表を取得し、そこから期待死亡数を組み立てています。隣接関係はjapan_adj.csvという別ファイルから読み込み、隣接行列に変換しています。
ここから先は、データの取得と整形の実装が大半を占めます。階層ベイズの主張を理解するうえで必須の部分ではありません。モデル定義の中核は後半の10行ほどで、そこだけを追えば構造は把握できます。実装の詳細に関心がない場合は、コードのあとに置いた推定結果まで読み飛ばしていただいて差し支えありません。
import re
import csv
import numpy as np
import pandas as pd
import urllib.request
import folium
from IPython.display import display
import pymc3 as pm
%matplotlib inline
import matplotlib.pylab as plt
appid = "YOUR APPLICATION ID"
api_version = "2.1"
base_url = "http://api.e-stat.go.jp/rest/{api_version}/app/".format(api_version=api_version)
get_type = "getStatsList"
#stats_code = "00450011" # 人口動態調査
#stats_code = "00200524" # 人口推計
#stats_code = "00200521" # 国勢調査
#print(base_url + "{}?appId={}&statsCode={}".format(get_type, appid, stats_code))
# 都道府県別の自殺数
get_type="getStatsData"
stats_data_id="0003030127"
url =base_url + "{}?appId={}&statsDataId={}".format(get_type, appid, stats_data_id)
#print(url)
get_type="getSimpleStatsData"
stats_data_id="0003030127"
lv_cat_01="2"
cd_cat_02="129"
cd_cat_03="1"
section_header_flg="2"
url = base_url + "{}?appId={}&statsDataId={}&lvCat01={}&cdCat02={}&cdCat03={}§ionHeaderFlg={}".format(get_type, appid, stats_data_id, lv_cat_01, cd_cat_02, cd_cat_03, section_header_flg)
data_pref_die = urllib.request.urlopen(url).read().decode("utf8")
# 都道府県別、年齢階級別の人口
get_type="getStatsData"
stats_data_id="0003014716"
url = base_url + "{}?appId={}&statsDataId={}".format(get_type, appid, stats_data_id)
#print(url)
get_type="getSimpleStatsData"
stats_data_id="0003014716"
cd_cat_01="000"
cd_cat_02_from="01001"
cd_cat_02_to="04018"
lv_area="2"
section_header_flg="2"
url = base_url + "{}?appId={}&statsDataId={}&cdCat01={}&cdCat02From={}&cdCat02To={}&lvArea={}§ionHeaderFlg={}".format(get_type, appid, stats_data_id, cd_cat_01, cd_cat_02_from, cd_cat_02_to, lv_area, section_header_flg)
data_pref_age_pop = urllib.request.urlopen(url).read().decode("utf8")
# 全国、年齢階級別の自殺数
get_type="getStatsData"
stats_data_id="0003031497"
url =base_url + "{}?appId={}&statsDataId={}".format(get_type, appid, stats_data_id)
#print(url)
get_type="getSimpleStatsData"
stats_data_id="0003031497"
lv_cat_01="2"
cd_cat_02="268"
cd_cat_03="1"
section_header_flg="2"
url = base_url + "{}?appId={}&statsDataId={}&lvCat01={}&cdCat02={}&cdCat03={}§ionHeaderFlg={}".format(get_type, appid, stats_data_id, lv_cat_01, cd_cat_02, cd_cat_03, section_header_flg)
data_all_age_die = urllib.request.urlopen(url).read().decode("utf8")
# 全国、年齢階級別の人口
get_type="getStatsData"
stats_data_id="0003014709"
url = base_url + "{}?appId={}&statsDataId={}".format(get_type, appid, stats_data_id)
#print(url)
get_type="getSimpleStatsData"
stats_data_id="0003014709"
cd_cat_01 = "000"
cd_cat_02 = "001"
cd_cat_03_from = "01001"
cd_cat_03_to = "01021"
cd_time = "2009000000"
section_header_flg="2"
url = base_url + "{}?appId={}&statsDataId={}&cdCat01={}&cdCat02={}&cdCat03From={}&cdCat03To={}&cdTime={}§ionHeaderFlg={}".format(get_type, appid, stats_data_id, cd_cat_01, cd_cat_02, cd_cat_03_from, cd_cat_03_to, cd_time, section_header_flg)
data_all_age_pop = urllib.request.urlopen(url).read().decode("utf8")
# 都道府県別の自殺数、データフレーム化
dlines = data_pref_die.splitlines()[2:]
jiscode, cnt = [], []
for line in dlines:
line2 = line.replace('"', "").split(",")
jiscode_tmp = line2[2]
jiscode_tmp = int(jiscode_tmp)-1
if jiscode_tmp > 47:
continue
jiscode_tmp = "0" + str(jiscode_tmp) if jiscode_tmp < 10 else str(jiscode_tmp)
jiscode.append(jiscode_tmp)
cnt_tmp = int(line2[11])
cnt.append(cnt_tmp)
df_pref_die = pd.DataFrame({"jiscode": jiscode, "cnt": cnt})
# 都道府県コードマスタ、都道府県別、年齢階級別の人口、データフレーム化
dlines = data_pref_age_pop.splitlines()[2:]
jiscode, name, age_cls, cnt = [], [], [], []
for line in dlines:
line2 = line.replace('"', "").split(",")
jiscode_tmp = line2[6]
jiscode_tmp = jiscode_tmp.replace("000", "")
jiscode.append(jiscode_tmp)
name_tmp = line2[7]
name.append(name_tmp)
age_cls_tmp = line2[5]
age_cls_tmp = age_cls_tmp.replace("歳", "").split("~")
age_cls_tmp = "85_" if len(age_cls_tmp) == 1 else age_cls_tmp[0]+"_"+age_cls_tmp[1]
age_cls.append(age_cls_tmp)
cnt_tmp = line2[11]
cnt_tmp = int(cnt_tmp)*1000
cnt.append(cnt_tmp)
df_pref_age_pop = pd.DataFrame({"jiscode": jiscode, "age_cls": age_cls, "cnt": cnt})
df_pref_age_pop_14 = df_pref_age_pop.query("age_cls=='0_4' | age_cls=='5_9' | age_cls=='10_14'")
df_pref_age_pop_14 = df_pref_age_pop_14[["jiscode", "cnt"]].groupby("jiscode").sum().reset_index()
col14 = pd.DataFrame([["_14"]*47]).T
col14.columns = ["age_cls"]
df_pref_age_pop_14 = pd.concat([df_pref_age_pop_14, col14], axis=1)
df_pref_age_pop = df_pref_age_pop.query("age_cls!='0_4' & age_cls!='5_9' & age_cls!='10_14'")
df_pref_age_pop = pd.concat([df_pref_age_pop, df_pref_age_pop_14], axis=0)
jiscode = sorted(set(jiscode), key=jiscode.index)
name = sorted(set(name), key=name.index)
df_pref_mst = pd.DataFrame({"jiscode": jiscode, "name": name})
# 全国の年齢階級別の自殺数、データフレーム化
dlines = data_all_age_die.splitlines()[2:]
age_cls, cnt = [], []
for line in dlines:
line2 = line.replace('"', "").split(",")
age_cls_tmp = line2[3]
if age_cls_tmp == "不詳":
continue
age_cls_tmp = re.sub(r"歳|~", "", age_cls_tmp)
age_cls_tmp = "100_" if age_cls_tmp == "100" else str(int(age_cls_tmp.split("-")[0]))+"_"+str(int(age_cls_tmp.split("-")[-1]))
age_cls.append(age_cls_tmp)
cnt_tmp = line2[-1]
cnt_tmp = 0 if cnt_tmp == "-" else int(cnt_tmp)
cnt.append(cnt_tmp)
age_cls = age_cls[3:-3]
age_cls.insert(0, "_14")
age_cls[-1] = "85_"
cnt_15 = np.sum(cnt[0:3])
cnt_85 = np.sum(cnt[-4:])
cnt = cnt[3:-4]
cnt.insert(0, cnt_15)
cnt.append(cnt_85)
df_all_age_die = pd.DataFrame({"age_cls" : age_cls, "cnt" : cnt})
# 全国の年齢階級別の人口、データフレーム化
dlines = data_all_age_pop.splitlines()[2:]
age_cls, cnt = [], []
for line in dlines:
line2 = line.replace('"', "").split(",")
age_cls_tmp = line2[5]
age_cls_tmp = re.sub(r"歳", "", age_cls_tmp).split("~")
age_cls_tmp = "100_" if len(age_cls_tmp) == 1 else age_cls_tmp[0]+"_"+age_cls_tmp[1]
age_cls.append(age_cls_tmp)
cnt.append(int(line2[-1])*1000)
age_cls = age_cls[3:-3]
age_cls.insert(0, "_14")
age_cls[-1] = "85_"
cnt_15 = np.sum(cnt[0:3])
cnt_85 = np.sum(cnt[-4:])
cnt = cnt[3:-4]
cnt.insert(0, cnt_15)
cnt.append(cnt_85)
df_all_age_pop = pd.DataFrame({"age_cls" : age_cls, "cnt" : cnt})
# 都道府県別、期待死亡数、データフレーム
df_y_tmp = pd.merge(df_all_age_pop, df_all_age_die, how="inner", on="age_cls", suffixes=("_pop", "_die"))
df_y_tmp = df_y_tmp.assign(y_tmp=df_y_tmp["cnt_die"]/df_y_tmp["cnt_pop"])
df_y_tmp = df_y_tmp.drop("cnt_pop", axis=1).drop("cnt_die", axis=1)
df_y_tmp = pd.merge(df_pref_age_pop, df_y_tmp, how="inner", on="age_cls")
df_y_tmp = df_y_tmp.assign(y=df_y_tmp["y_tmp"]*df_y_tmp["cnt"])
df_y_tmp = df_y_tmp.drop("cnt", axis=1).drop("y_tmp", axis=1)
df_y = df_y_tmp[["jiscode", "y"]].groupby("jiscode").sum().reset_index()
# 都道府県別、隣接都道府県、データフレーム
from_names, to_names = [], []
with open("japan_adj.csv", "r") as f:
reader = csv.reader(f)
for row in reader:
from_name_tmp = row[1].replace("\xa0", "").replace(" ", "")
to_names_tmp = row[4].replace("\xa0", "").replace(" ", "").split(",")
for to_name_tmp in to_names_tmp:
from_names.append(from_name_tmp)
to_names.append(to_name_tmp)
from_names = from_names[0:-1]
to_names = to_names[0:-1]
df_adj_tmp = pd.DataFrame({"f_name": from_names, "t_name": to_names})
df_adj_tmp = pd.merge(df_adj_tmp, df_pref_mst, how="left", left_on="f_name", right_on="name").drop("name", axis=1).rename(columns={"jiscode": "f_jiscode"})
df_adj_tmp = pd.merge(df_adj_tmp, df_pref_mst, how="left", left_on="t_name", right_on="name").drop("name", axis=1).rename(columns={"jiscode": "t_jiscode"})
df_adj = df_adj_tmp.drop("f_name", axis=1).drop("t_name", axis=1)
# 自殺数、期待死亡数、隣接行列 for 階層ベイズモデル
jiscodes = list(df_pref_mst["jiscode"].values.flatten())
n = len(jiscodes)
y_samples, z_samples, adj_matrix, d_matrix = [], [], [], []
for i in range(0, n):
y = df_y[df_y["jiscode"] == jiscodes[i]]["y"].values[0] # 期待死亡数
y_samples.append(y)
z = df_pref_die[df_pref_die["jiscode"] == jiscodes[i]]["cnt"].values[0] # 自殺数
z_samples.append(z)
adjs = df_adj[df_adj["f_jiscode"] == jiscodes[i]]["t_jiscode"].values # 隣接JISCODE
adjs_idx = np.zeros(n)
for j in adjs:
adjs_idx[jiscodes.index(j)] = 1
adj_matrix.append(adjs_idx)
d_idx = np.zeros(n)
d_idx[i] = 2
d_matrix.append(d_idx)
jiscodes = np.array(jiscodes, dtype=object) # JISCODE
y_samples = np.array(y_samples, dtype=int) # 期待死亡数
z_samples = np.array(z_samples, dtype=int) # 自殺数
adj_matrix = np.array(adj_matrix, dtype=int).reshape(n, n) # 隣接行列
d_matrix = np.array(d_matrix, dtype=int).reshape(n, n) # 自身の行列
# ベイズモデルで推定
with pm.Model() as model:
p1 = pm.Uniform("p1", lower=0, upper=1)
p2 = pm.Uniform("p2", lower=0, upper=1)
tau_phi = pm.Gamma("tau_phi", alpha=0.5, beta=0.005)
phi = pm.Normal("phi", mu=0, tau=tau_phi)
t = p1*d_matrix+p2*adj_matrix
psi = pm.MvNormal('psi', mu=0, cov=t, shape=n)
lamb = np.exp(np.log(y_samples)+phi+psi)
z = pm.Poisson("z", mu=lamb, observed=z_samples)
start = pm.find_MAP()
step = pm.NUTS()
trace = pm.sample(1000, step, start)
#pm.traceplot(trace)
#pm.summary(trace)
phi_m = np.mean(trace["phi"])
psi_m = []
t = trace["psi"].transpose()
for i in t:
psi_m.append(np.mean(i))
smr_est = np.exp(phi_m+psi_m)
# 都道府県別、推定SMR
df_smr_est = pd.DataFrame({"jiscode" : jiscodes, "smr_est" : smr_est})
display(df_smr_est)
# コロプレスマップに推定値を可視化
location = [35.709634, 139.392101]
tiles="Stamen Toner"
zoom_start = 10
map = folium.Map(location=location, tiles=tiles, zoom_start=zoom_start)
map.choropleth(
geo_path="japan.geojson",
data=df_smr_est,
columns=["jiscode", "smr_est"],
key_on="properties.JCODE",
threshold_scale=[0.90, 0.95, 1.00, 1.05, 1.10, 1.15],
fill_color="YlGnBu",
fill_opacity=0.7,
line_opacity=0.3,
reset=True)
display(map)

都道府県別のリスクを推定した結果です。地方の方がリスクが高く推定される傾向が出ており、期待死亡数についても全体的に地方の方が高い傾向にありました。
コードの中でモデル定義にあたるのはwith pm.Model() as model:以下の10行ほどです。tau_phiがガンマ分布に従う精度パラメータで、\( \phi \) の分布の広がりを決めています。この値がデータから推定されることで、全国平均へ引き寄せる強さが自動的に決まります。p1とp2は、自分自身の行列と隣接行列を混ぜる重みで、これも一様分布を事前分布として与えて推定させています。空間相関をどれだけ強く効かせるかについても、値を決め打ちせずデータに委ねる形にしました。塗り分けの閾値を0.90から1.15の範囲に取っているのは、推定されたSMRがおおむね1の近くに収まったためで、これ自体が縮約が働いていることを示しています。
実装上、明確に残った課題があります。地域相関を入れるCAR事前分布の部分は、Stanではパッケージが自動的に処理してくれますが、当時のPyMCにはそれがなく自前で構築する必要がありました。とくに、多変量ガウス分布の分散共分散行列で地域相関を表現させる際の重みの調整をどうすればよいかが定まらず、結局そこにも事前分布を入れて回してみる、という形にしています。
この点は現在では状況が変わっています。空間統計モデリングにおいて、CARモデルの実装がPyMCでも公式にサポートされるようになっており、以前のように自前で共分散行列を構築する必要性は低下しました。あわせて、PyMC3は現在PyMC(バージョン5系)として大幅にリニューアルされ、内部のサンプリングエンジンがTheanoからPyTensorに移行しています。上記のコードをそのまま最新のPyMCで実行するにはAPI変更に合わせた書き換えが必要です。foliumも継続的にアップデートされており、コロプレスマップの作成メソッドがchoroplethからChoroplethクラスベースに変更されるなど、インターフェースが変わっています。e-Stat APIのバージョンも更新が進んでいますので、最新の仕様は公式サイトで確認してください。
もう一点、実務で階層モデルを組むときに共通して効いてくる注意として、推定結果を出したあとに収束の確認を省かないことがあります。階層モデルは事後分布の形が難しく、見た目の推定値は出ていてもサンプラーが十分に探索できていないことがあります。第4章で扱った収束診断の指標と、発散した遷移の警告は必ず確認する必要があります。
ここまで見てきた構造は、地域を別のグループに置き換えればそのまま移せます。地域別自殺リスクの推定で使った要素を並べ替えると、次のような対応になります。
| この例の要素 | 対応するビジネスの要素 | 具体例 |
|---|---|---|
| 都道府県(47グループ) | 店舗・営業所・製品・担当者 | 200店舗、30営業所、500SKU |
| 観測死亡数 \( z_i \) | 件数として観測される結果 | 成約数、故障件数、クレーム件数 |
| 期待死亡数 \( y_i \) | 規模で決まる基準量 | 来店客数、稼働台数、出荷数 |
| 年齢構成の標準化 | 条件差の調整 | 商圏人口、設備年齢、顧客層 |
| SMR | 基準に対する相対指標 | 期待比の成約率、期待比の故障率 |
| 隣接関係 | グループ間の近さの構造 | 同一エリア、同一業態、同一製品系列 |
代表的な使いどころを三つ挙げます。
一つめは店舗別・営業所別の実力評価です。件数の少ない小規模拠点が上位と下位を独占するランキングは、意思決定の材料になりません。階層モデルで縮約をかけた推定値を使うと、上位下位に残るのは「少数データの偶然では説明できない差」だけになります。表彰や改善対象の選定に使う数字としては、こちらの方が妥当です。あわせて事後分布の幅を見れば、「差はありそうだが確信は持てない拠点」を区別できます。
二つめは新規拠点・新製品の初期予測です。完全分離のモデルではデータがなければ何も出せませんが、階層モデルは全体分布をそのまま予測に使えます。さらに、開店から数か月のデータが溜まるにつれて、推定値が全体平均から自店の実績へ滑らかに移っていきます。「何件溜まったら自店の数字を信じてよいか」という運用ルールを人手で決める必要がなくなり、重み \( w_g \) がその判断を代行します。
三つめは効果測定です。施策を一部の拠点に投入したときの効果を拠点別に測ろうとすると、拠点あたりのサンプルはさらに小さくなります。ここで拠点別に独立に効果を推定すると、効果があったように見える拠点となかったように見える拠点がまだらに出て、解釈が困難になります。効果そのものを階層構造に載せれば、全体としての平均効果と、拠点ごとのばらつきの大きさを同時に推定できます。「平均的には効いているが、拠点差が大きい」のか「全拠点でほぼ同じだけ効いている」のかを分けて答えられる点が、横展開の判断に直結します。
導入にあたっての前提も整理しておきます。階層モデルが力を発揮するのは、グループ数がある程度多く(目安として10以上)、グループごとのデータ量に偏りがあり、グループ間に共通の構造があると考えてよい場合です。逆に、グループが3つしかない場合や、各グループが本質的に別物で共通の分布を仮定できない場合には、無理に階層化する必要はありません。グループを共通の分布から生まれたものとして扱ってよいかという判断は、統計の問題である前に業務の問題です。この判断だけは、データではなく現場の知識から決めることになります。
『データ解析のための統計モデリング入門 一般化線形モデル・階層ベイズモデル・MCMC』(久保拓弥、岩波書店):一般化線形モデルから階層ベイズまでを、なぜその拡張が必要になるのかという順序で説明した定番書です。個体差・場所差をどう扱うかという問題意識がこの章の内容と直結します。
『岩波データサイエンス Vol.4』(岩波データサイエンス刊行委員会編、岩波書店):ベイズ推定とMCMCのフリーソフトを特集した巻で、空間的な相関を考慮した階層ベイズモデルによる地域別リスク推定の事例が収録されています。この章で扱った実例の下敷きにあたる内容です。
手元にある記録が「AがBに勝った」「BがCに勝った」という勝敗の羅列だけだったとします。ここから各参加者の実力を数値にしたい、というのがこの章で扱う問題です。素朴な方法は勝率を計算することですが、勝率は対戦相手の顔ぶれを無視します。弱い相手とばかり当たった選手の8割勝利と、上位陣とばかり当たった選手の5割勝利では、同じ数字が持つ意味がまったく違います。勝率という指標は、誰と戦ったかという情報を捨ててしまうのです。
もうひとつの問題は試合数です。3試合して3勝した選手の勝率は1.0ですが、その1.0は50試合して40勝した選手の0.8より信頼できるでしょうか。点推定の数値だけを並べると、この差は表面に出てきません。第3章で扱ったとおり、ベイズ推定は結果を分布として返しますから、「強いという推定だが幅が広い」という状態をそのまま表現できます。データの少ない参加者については、推定値が高く出ていても幅が広い、という形で不確かさが可視化されます。
この章では、男子プロテニスの対戦成績を題材に、勝敗という記録だけから各選手の強さを推定するベイズモデルを組み立てます。さらにそれを時系列に拡張し、各選手の強さが年ごとにどう推移したかを追います。使うデータは、Kaggleで公開されているATPツアーの試合結果、Association of Tennis Professionals Matches(2000年から2017年までのトーナメント結果)です。データには大会名・ツアーレベル・開催日・サーフェス・得点情報など多くの列が含まれていますが、ここで使うのは「試合が開催された年」「その試合に勝った選手」「負けた選手」の3つだけです。スコアもサーフェスも使いません。それでも、後で見るとおり、実感に合う強さの序列が得られます。
考え方の中心は、観測される勝敗という0か1かの離散的な結果の背後に、目には見えない連続量を置くことです。この見えない連続量が各参加者の強さであり、統計モデルの言葉では潜在変数と呼びます。潜在変数とは、直接は測定できないものの、観測されたデータを生み出している原因として仮定される量のことです。テストの点数の背後に学力を置く、購買履歴の背後に嗜好を置く、といった発想と同じ枠組みです。
対戦データにこの発想を適用したものがBradley-Terryモデルと呼ばれる一群のモデルです。基本形は単純で、参加者\( i \)と\( j \)がそれぞれ強さ\( \lambda_i \)、\( \lambda_j \)を持ち、\( i \)が\( j \)に勝つ確率を強さの差の関数として書きます。
\( P(i \text{ が } j \text{ に勝つ}) = \dfrac{\exp(\lambda_i)}{\exp(\lambda_i) + \exp(\lambda_j)} \)
強さが等しければ勝率は0.5になり、差が開くほど勝率が1または0に近づきます。将棋やチェスのレーティング、オンラインゲームのマッチメイキングで使われる仕組みも、発想としてはこの系列に属します。重要なのは、この形が「勝敗という2値の記録を、連続的な強さの差に翻訳する装置」になっている点です。誰と戦って勝ったのかという情報が、相手の\( \lambda_j \)を通じて自動的に効いてきます。強い相手に勝てば自分の\( \lambda_i \)を大きく押し上げ、弱い相手に負ければ大きく引き下げる。勝率の単純計算にはなかった、対戦相手の考慮が構造として組み込まれています。
この章で使うモデルは、Bradley-Terryの発想を保ちながら、もう一段の要素を足したものです。各選手は固有の強さを持つと同時に、試合ごとの出来不出来があります。調子よく本来の力を出せた日もあれば、不調で力を出しきれなかった日もあるはずです。この試合ごとの揺らぎの大きさを選手ごとのパラメータとして持たせ、勝負ムラと呼ぶことにします。強さは選手の水準を、勝負ムラは水準からの散らばりを表します。実務の言葉に置き換えれば、平均的な実力と、その安定性を分けて推定するということです。
ここで使うモデルは次のように書けます。参考にしたのは『StanとRでベイズ統計モデリング』に収録されている棋士の強さのモデリングで、その定式化をテニスの対戦データに適用しました。
\( performance[g, 1] \sim Normal(\mu[Loser], \sigma_{pf}[Loser]), \quad g = 1, \ldots, G \) \( performance[g, 2] \sim Normal(\mu[Winner], \sigma_{pf}[Winner]), \quad g = 1, \ldots, G \) \( performance[g, 1] < performance[g, 2], \quad g = 1, \ldots, G \) \( \mu[n] \sim Normal(0, \sigma_{\mu}), \quad n = 1, \ldots, N \) \( \sigma_{pf}[n] \sim Gamma(10, 10), \quad n = 1, \ldots, N \)
記号を順に説明します。\( Winner \)は勝った選手のインデックス、\( Loser \)は負けた選手のインデックスです。各選手の強さを\( \mu[n] \)、勝負ムラを\( \sigma_{pf}[n] \)とし、1回の勝負で発揮する力、すなわちパフォーマンスは、平均\( \mu[n] \)、標準偏差\( \sigma_{pf}[n] \)の正規分布から生成されると考えます。各選手はそれぞれ強さの値を持っているのですが、試合ごとに見れば、調子が良くてパフォーマンスを十分に発揮できた時もあれば、不調で本来の力を出せなかった時もあるはずで、その変化の大きさをムラとして捉えるイメージです。
そして3行目が、このモデルの核心にあたります。勝負の結果はパフォーマンスの大小で決まる、つまり大きかった方が勝つ、という制約です。観測されているのは「誰が勝ったか」だけで、パフォーマンスの実数値そのものは記録されていません。パフォーマンスは推定すべき未知量であり、勝敗という観測はその大小関係にだけ制約を与えます。1試合ごとに、負けた側のパフォーマンスは勝った側より小さい。この不等式が\( G \)試合ぶん積み重なることで、各選手の\( \mu \)と\( \sigma_{pf} \)が絞り込まれていきます。
4行目は強さの事前分布で、平均0の正規分布を置いています。強さは相対的な量なので、全体の基準をどこかに固定しないと値が定まりません。平均0という指定がその役割を果たします。5行目の勝負ムラにはガンマ分布を置きました。ガンマ分布は、プロットしてみれば分かりますが、正規分布などのような釣り鐘型の分布で、平均が1付近かつ正の値を取るような形状をしています。正の値をとって正規分布のような形をしてほしいという事前情報を与えたい時に、こういった分布を用います。標準偏差は負にならない量なので、正の範囲に台を持つ分布を選ぶ必要があるわけです。
分析の対象期間は、ひとまず直近のものとして2015年から2017年2月頃、データの最新期間までの戦績データを使います。分析対象選手は、この期間中の戦績データ数を確認しながら手動で選定しました。世界ランキング上位100名すべてを対象にすることも考えられますが、処理が膨大になり結果の解釈も難しくなるため、数名に絞ります。データ数との兼ね合いで、以下の選手リストを用意しました。
なお、本章に載せたコードと実行結果は、PyStan 2系と当時のStan構文で実装したときの実測です。現行の環境で動かす場合は、次のように読み替えてください。pystan.stan()やfit.extract()はPyStan 2系のAPIなので、PyStan 3ではimport stan、stan.build()、posterior.sample()を使います。第5章で標準として紹介したCmdStanPyに揃えるのも良い選択です。Stanコードの配列宣言も、Stan 2.33以降はint LW[G, 2]のような旧構文がエラーになるため、array[G, 2] int<lower=1, upper=N> LW;の形に書き換える必要があります。
ARR_TARGET_PLAYER = np.array([
'Roger Federer',
'Rafael Nadal',
'Novak Djokovic',
'Andy Murray',
'Stanislas Wawrinka',
'Juan Martin Del Potro',
'Milos Raonic',
'Kei Nishikori',
'Gael Monfils',
'Tomas Berdych',
'Jo Wilfried Tsonga',
'David Ferrer',
'Richard Gasquet',
'Marin Cilic',
'Grigor Dimitrov',
'Dominic Thiem',
'Nick Kyrgios',
'Alexander Zverev'
])いわゆるBIG4であるフェデラー、ナダル、ジョコビッチ、マレーを軸に、錦織、デル・ポトロ、ラオニッチなどの中堅、ベルディヒ、ツォンガ、フェレールなど長くトップ10周辺に位置してきたベテラン、そして当時活躍してきていたティエム、キリオス、ズベレフなどの若手選手を加えた構成です。
次に、各選手の合計試合数と勝率を確認します。対象選手同士の対戦だけを抽出するため、勝者と敗者の両方がリストに含まれる試合に絞り込んでいます。
START_YEAR = 2015
# Use columns
df_matches = df_matches[["year", "winner_name", "loser_name"]]
# Convert data type
df_matches["year"] = df_matches["year"].astype(int)
df_matches["winner_name"] = df_matches["winner_name"].astype(str)
df_matches["loser_name"] = df_matches["loser_name"].astype(str)
# Extract target player records
df_matches = df_matches[
(df_matches['year'] >= START_YEAR) &
(df_matches['winner_name'].isin(ARR_TARGET_PLAYER)) &
(df_matches['loser_name'].isin(ARR_TARGET_PLAYER))
]
def plot_n_match_win_rate(df, arr_target_player):
"""Plot the number of matchs and win rates for each players.
Args:
df (pd.DataFrame): df_matches.
arr_target_player (np.ndarray): Target player list.
"""
arr_cnt = []
arr_rate = []
for player in arr_target_player:
cnt_win = len(df[df['winner_name'] == player])
cnt_lose = len(df[df['loser_name'] == player])
arr_cnt.append(cnt_win + cnt_lose)
arr_rate.append(cnt_win / (cnt_win + cnt_lose))
fig, axs = plt.subplots(ncols=2, figsize=(15, 5))
axs[0].bar(x=arr_target_player, height=arr_cnt, color='b', alpha=0.5)
axs[0].set(xlabel='player', ylabel='cnt')
for tick in axs[0].get_xticklabels():
tick.set_rotation(75)
axs[0].set_title("Plot the number of matches")
axs[1].bar(x=arr_target_player, height=arr_rate, color='r', alpha=0.5)
axs[1].set(xlabel='player', ylabel='rate')
for tick in axs[1].get_xticklabels():
tick.set_rotation(75)
axs[1].set_title("Plot the rate of win")
plt.show()
plot_n_match_win_rate(df_matches, ARR_TARGET_PLAYER)
青が対象期間中の各選手の試合数、対象選手同士の対戦に限ったものです。赤が勝率を表しています。ベイズモデリングといえどもデータ数が少なすぎれば収束しない恐れがあり、デル・ポトロなどは若干データが少なめですが、収束しなければ外すこととして、ひとまずこのまま進めます。
続いて、統計モデルに合わせるデータ形式を用意します。モデルが必要としているのは、1試合につき「負けた選手のインデックス、勝った選手のインデックス」という2つの整数の組です。
def get_dict_target(arr_target):
"""Get dictionary of value of arr_target to number.
Args:
arr_target (np.ndarray): List.
Returns:
dic_target (dict[str, int]): Dictionary.
"""
dic_target = {}
for v in arr_target:
if v not in dic_target:
dic_target[v] = len(dic_target) + 1
return dic_target
def get_lw(df, arr_target_player, dic_target_player):
"""Get LW data for input model.
Args:
df (pd.DataFrame): df_matches.
arr_target_player (np.ndarray): Target player list.
dic_target_player (dict[str, int]): Dictionary of player to number.
Returns:
np.ndarray: LW array.
"""
LW = []
for player_a in arr_target_player:
for player_b in arr_target_player:
df_tmp = df[
(df['winner_name'] == player_a) &
(df['loser_name'] == player_b)
]
for _ in range(len(df_tmp)):
LW.append([dic_target_player[player_b], dic_target_player[player_a]])
df_tmp = df[
(df['winner_name'] == player_b) &
(df['loser_name'] == player_a)
]
for _ in range(len(df_tmp)):
LW.append([dic_target_player[player_a], dic_target_player[player_b]])
LW = np.array(LW, dtype=np.int32)
return LW
dic_target_player = get_dict_target(ARR_TARGET_PLAYER)
LW = get_lw(df_matches, ARR_TARGET_PLAYER, dic_target_player)上記のLWは「負け選手インデックス、勝ち選手インデックス」という順に試合ごとの行を格納した2次元配列で、これがパフォーマンスの大小を調整するためのデータとなります。行数がそのまま試合数\( G \)になります。モデルに渡す情報は、突き詰めればこの表だけです。選手名も、大会の格も、スコアも入っていません。
統計モデルをStanで書いて学習させます。
def train_model(data, n_iter, n_chains):
"""Train the model.
Args:
data (dict[str, int or np.ndarray]): Input data.
n_iter (int): The number of iter.
n_chains (int): The number of chain.
Returns:
collections.OrderedDict: Result.
"""
model = """
data {
int N;
int G;
int LW[G, 2];
}
parameters {
ordered[2] performance[G];
vector[N] mu;
real s_mu;
vector[N] s_pf;
}
model {
for (g in 1:G)
for (i in 1:2)
performance[g, i] ~ normal(mu[LW[g, i]], s_pf[LW[g, i]]);
mu ~ normal(0, s_mu);
s_pf ~ gamma(10, 10);
}
"""
fit = pystan.stan(model_code=model, data=data, iter=n_iter, chains=n_chains)
return fit.extract()
N_ITER = 1000
N_CHAINS = 4
data = {"N": len(dic_target_player), "G": len(LW), "LW": LW}
la = train_model(data, N_ITER, N_CHAINS) 上記コードのモデル部分のordered[2] performance[g]が、このモデルで重要なポイントです。これは順序制約付きパラメータで、1列目より2列目の方が大きいという制約を意味しています。performance[g, j] ~ normal(mu[LW[g, j]], s_pf[LW[g, j]])ではLWの負けプレイヤーインデックス側のパフォーマンスよりも、勝ちプレイヤーインデックス側のパフォーマンスの方が大きくなるよう調整が行われます。
ここが、勝敗という観測をモデルに入力している箇所にあたります。通常の回帰であれば観測値そのものを尤度に渡しますが、この設計では観測されているのは大小関係だけです。その大小関係を、パラメータ側の宣言によって表現しています。ordered型を使うと、Stanは内部で順序を保つ変数変換を行い、サンプリング中に制約が破られないようにします。第4章で扱ったMCMCの言葉でいえば、探索の対象となる空間そのものを「1列目より2列目が大きい」という領域に限定しているということです。制約を後から弾くのではなく、はじめから満たす形で探索するため、効率よく収束します。
また、このorderedの長さを変えるだけで、対戦以外の順位データにも同じモデルが使えます。3人のうち誰が1位かという回答であればordered[3]、5着までの着順であればordered[5]という具合です。この拡張性については章の終わりで改めて触れます。
処理を回してみたところ、すべての主要パラメータでR-hatが1.01を下回り、収束の判定基準を満たしていました。R-hatは複数のチェイン、すなわち独立に走らせた複数のMCMCの系列が、同じ分布に落ち着いているかを測る指標で、1.00に近いほど良好とされます。複数の登山者が別々の地点から出発して、最終的に同じ地形図を描いたかどうかを確かめる作業だと考えると分かりやすいでしょう。念のため、各チェインごとのサンプリングをプロットしてみた結果が以下になります。
def plot_chains_dist(arr_target_player, n_chains, la, target_param):
"""Plot distributions of each chains.
Args:
arr_target_player (np.ndarray): Target player list.
n_chains (int): The number of chains.
la (collections.OrderedDict): Result.
target_param (str): Target latest parameter.
"""
cmap = matplotlib.cm.get_cmap('tab10')
plt.figure(figsize=(10, 5))
for i, player in enumerate(arr_target_player):
for j in range(n_chains):
g = plt.violinplot(
la[target_param][j * 500:(j + 1) * 500, i],
positions=[i],
showmeans=False,
showextrema=False,
showmedians=False,
)
for pc in g['bodies']:
pc.set_facecolor(cmap(j))
plt.legend([f"chain {i + 1}" for i in range(n_chains)])
plt.xticks(list(range(len(arr_target_player))), arr_target_player)
plt.xticks(rotation=45)
plt.xlabel('player')
plt.ylabel(target_param)
plt.show()
plot_chains_dist(
arr_target_player=ARR_TARGET_PLAYER,
n_chains=N_CHAINS,
la=la,
target_param="mu",
)
plot_chains_dist(
arr_target_player=ARR_TARGET_PLAYER,
n_chains=N_CHAINS,
la=la,
target_param="s_pf",
)

4本のチェインの分布が良い感じに重なってちゃんと収束していそうなので、ひとまず問題なさそうです。もしチェインごとに分布の位置がずれていれば、それは推定が定まっていない合図であり、結果を読む前にモデルやデータを見直す必要があります。収束の確認を飛ばして数値の解釈に進むのは、測定器の校正をせずに測定値を報告するのと同じことです。
改めて統合したサンプリング結果をプロットしてみます。まずは各選手の強さ\( \mu \)の事後分布です。
def plot_posterior_dist(arr_target_player, la, target_param):
"""Plot posterior distributeion of given latest parameter.
Args:
arr_target_player (np.ndarray): Target player list.
la (collections.OrderedDict): Result.
target_param (str): Target latest parameter.
"""
cmap = matplotlib.cm.get_cmap('tab10')
plt.figure(figsize=(15, 7))
for i, player in enumerate(arr_target_player):
g = plt.violinplot(
la[target_param][:, i],
positions=[i],
showmeans=False,
showextrema=True,
showmedians=True,
)
c = cmap(i % 10)
for pc in g['bodies']:
pc.set_facecolor(c)
g['cbars'].set_edgecolor(c)
g['cmaxes'].set_edgecolor(c)
g['cmedians'].set_edgecolor(c)
g['cmins'].set_edgecolor(c)
plt.xticks(list(range(len(arr_target_player))), arr_target_player)
plt.xticks(rotation=45)
plt.xlabel('player')
plt.ylabel(target_param)
plt.show()
def get_desc_posterior_dist(arr_target_player, n_chains, la, target_param):
"""Get summary of posterior distribution of given latest parameter.
Args:
arr_target_player (np.ndarray): Target player list.
n_chains (int): The number of chains.
la (collections.OrderedDict): Result.
target_param (str): Target latest parameter.
Returns:
pd.DataFrame: Dataframe of summary.
"""
summary = np.zeros((len(arr_target_player), n_chains))
for i, player in enumerate(arr_target_player):
samples = la[target_param][:, i]
median = np.median(samples, axis=0)
std = np.std(samples, ddof=1)
lower, upper = np.percentile(samples, q=[25.0, 75.0], axis=0)
summary[i] = [median, std, lower, upper]
df_summary = pd.DataFrame(
summary,
index=arr_target_player,
columns=['median', 'std', '25%', '75%'],
)
return df_summary
target_param = "mu"
plot_posterior_dist(
arr_target_player=ARR_TARGET_PLAYER,
la=la,
target_param=target_param,
)
df_s = get_desc_posterior_dist(
arr_target_player=ARR_TARGET_PLAYER,
n_chains=N_CHAINS,
la=la,
target_param=target_param,
)
display(df_s)

ヴァイオリンプロットが各選手の強さのプロットで、表はそれぞれ中央値・不偏標準偏差・50%ベイズ信頼区間の下限と上限となっています。対象期間がジョコビッチ1強時代であったため、ジョコビッチが一番高く出ており、直感通りの結果となりました。デル・ポトロもBIG4に引けを取らない強さとなっていますが、怪我などの影響で勝負回数が少ないからか、幅も大きく不確実度が高いことがわかります。意外に映るのは、ワウリンカと錦織が同じくらいの強さとなっている点です。
ここで注目したいのは、デル・ポトロの読み方です。中央値だけを見れば上位に位置しますが、分布の幅が広く、確信を持って上位と断じるだけの根拠はデータにありません。点推定の数値をランキング表に並べただけなら、この情報は失われます。分布として結果を受け取ることの実務的な価値が、こういう場面ではっきりします。「強い可能性が高いが、判断材料が足りない」という状態が、そのまま図に現れているのです。
推定結果が妥当かどうかは、元の対戦記録と照らして確かめます。対象期間中の戦績を可視化してみた結果が以下になります。

一気に確認できるようにしたかったため、あまり一般的な可視化ではないかもしれませんが、行が勝ちプレイヤー・列が負けプレイヤーであった試合が何回あったかをマス目の数値で表しています。これを見ても確かに、この期間で錦織はワウリンカに3勝2敗で勝ち越しており、ワウリンカ・錦織ともにBIG4にも勝ったりしているため、改めて錦織がトップ10の中でもかなり強い部類に位置していることが認識できました。また、対象期間中では若手は中堅以上に若干劣っている様子で、若手の中ではティエムが強いという結果になっているようです。
この突き合わせの作業には意味があります。モデルの出力が生データから読み取れる事実と整合しているかを確認する工程であり、整合していれば推定を信用する根拠が増え、食い違っていればモデルかデータのどちらかを疑う手がかりになります。ベイズモデルは事後分布という滑らかな出力を返しますが、その滑らかさが誤りを覆い隠す可能性は常にあります。
続いて、各選手の勝負ムラ\( \sigma_{pf} \)の事後分布を見ます。
target_param = "s_pf"
plot_posterior_dist(
arr_target_player=ARR_TARGET_PLAYER,
la=la,
target_param=target_param,
)
df_s = get_desc_posterior_dist(
arr_target_player=ARR_TARGET_PLAYER,
n_chains=N_CHAINS,
la=la,
target_param=target_param,
)
display(df_s)

見たところ、ワウリンカが勝負ムラが一番大きいようです。上記の戦績を見ても、BIG4に限らず同じ選手に勝ったり負けたりしていることがよくわかります。BIG4の中では、この期間は若干フェデラー、ナダルが勝ったり負けたりが多く、勝負ムラが少し大きめに出ています。デル・ポトロは、マレー、ワウリンカに1勝1敗で、それ以外は安定して勝っている戦績でしたが、勝負ムラは大きめに推定されました。この選手は対象期間の対戦数が少なく、事後分布の幅も広くなっています。データ数が少ない対象では推定が定まりにくく、値が大きめの側に振れることがあります。チリッチもこの期間はジョコビッチ、マレーに勝ったことがあり、その他選手とも勝ったり負けたりが多く、結果として勝負ムラが大きくなりました。興味深いのは、若手選手が全体的に勝負ムラが少し高めに出ている点で、これはまだパフォーマンスを安定して発揮することに慣れていないということなのかもしれません。
強さと勝負ムラを分けて推定したことで、単一の指標では表せない情報が取り出せています。水準は高いが安定しない参加者と、水準はやや劣るが安定している参加者は、勝率という1つの数字では区別できません。この区別は、実務でも意味を持ちます。例えば外注先の評価であれば、平均品質と品質のばらつきは別々に管理すべき性質のものです。1つの平均値に潰さず、2つのパラメータとして持たせる設計が、そのまま業務上の意思決定の材料になります。
ここまでは期間をある程度限定して分析しましたが、時系列で各選手の強さがどのように推移しているのかも見てみたいところです。そこで先ほどの統計モデルを拡張します。
\( performance[y, g, 1] \sim Normal(\mu[Loser[y]], \sigma_{pf}[Loser[y]]), \quad g = 1, \ldots, G, \quad y = 1, \ldots, Y \) \( performance[y, g, 2] \sim Normal(\mu[Winner[y]], \sigma_{pf}[Winner[y]]), \quad g = 1, \ldots, G, \quad y = 1, \ldots, Y \) \( performance[y, g, 1] < performance[y, g, 2], \quad g = 1, \ldots, G, \quad y = 1, \ldots, Y \) \( \mu[1][n] \sim Normal(0, \sigma_{\mu}), \quad n = 1, \ldots, N \) \( \mu[n][y] \sim Normal(\mu[n][y-1], t\_\sigma_{\mu}[n][y-1]), \quad n = 1, \ldots, N, \quad y = 2, \ldots, Y \) \( \sigma_{pf}[n] \sim Gamma(10, 10), \quad n = 1, \ldots, N, \quad y = 1, \ldots, Y \) \( \sigma_{\mu}[n][y] \sim Normal(0, 1), \quad n = 1, \ldots, N, \quad y = 1, \ldots, Y \)
各選手のy年の強さを\( \mu[n][y] \)、勝負ムラを\( \sigma_{pf}[n][y] \)とし、y年に行われる試合で発揮する力は、平均\( \mu[n][y] \)、標準偏差\( \sigma_{pf}[n][y] \)の正規分布から生成されるとします。勝負の結果は同様にパフォーマンスの大小で決まるとします。各選手のある年での強さ\( \mu[n][y] \)は、その1つ前の年の強さ\( \mu[n][y-1] \)から生成されると考えます。
5行目が拡張の中心です。今年の強さは昨年の強さの近くにある、という関係を分布の形で書いています。これは第7章で扱った階層構造の考え方と同じ性質を持ちます。年ごとに独立に強さを推定すると、試合数の少ない年の推定が暴れます。前年とのつながりを事前分布として与えることで、隣接する年から情報を借りて推定を安定させるわけです。時間方向の情報共有と言い換えてもよく、この発想は第10章の状態空間モデルへ直結します。
また、識別可能にするために、次の仮定を入れました。1つ目は「各選手の初年の強さは平均0、標準偏差\( \sigma_{\mu}[n][1] \)の半正規分布に従う」、2つ目は「各選手の年別の強さの変化量は\( \sigma_{\mu}[n][y] \)が半正規分布に従う」というもので、後者は弱情報事前分布です。弱情報事前分布とは、結論を決めてしまうほど強くはないが、あり得ない値を排除する程度には情報を持たせた事前分布のことです。無情報事前分布でも試してみましたが、うまく収束しませんでした。次年になると別人のようにいきなり強くなるということはあまり無いでしょうから、この程度の仮定はあっても問題ないと考えます。
分析の対象期間は、2005年から2017年2月頃の戦績データとします。この期間だと、前半で対象としていた若手選手などはほとんどデータが少なくなりすぎてしまうため、対象選手を絞り直して以下のように選出しました。
ARR_TARGET_PLAYER = np.array([
'Roger Federer',
'Rafael Nadal',
'Novak Djokovic',
'Andy Murray',
'Stanislas Wawrinka',
'Juan Martin Del Potro',
'Kei Nishikori',
'Tomas Berdych',
'David Ferrer',
])先ほどと同様に、対象期間の試合数と勝率の推移を可視化してみると以下のような感じです。
def plot_n_match_win_rate_ts(df, arr_target_year, arr_target_player):
"""Plot the number of matchs and win rates for each players on each years.
Args:
df (pd.DataFrame): df_matches.
arr_target_year (np.ndarray): Target year list.
arr_target_player (np.ndarray): Target player list.
"""
matrix_cnt = np.zeros((len(arr_target_year), len(arr_target_player)), dtype=np.float32)
matrix_rate = np.zeros((len(arr_target_year), len(arr_target_player)), dtype=np.float32)
for i, year in enumerate(arr_target_year):
for j, player in enumerate(arr_target_player):
cnt_win = len(df[(df['winner_name'] == player) & (df['year'] == year)])
cnt_lose = len(df[(df['loser_name'] == player) & (df['year'] == year)])
rate = 0 if (cnt_win + cnt_lose == 0) else cnt_win / (cnt_win + cnt_lose)
matrix_cnt[i, j] = cnt_win + cnt_lose
matrix_rate[i, j] = rate
for j, player in enumerate(arr_target_player):
if j % 3 == 0:
fig, axs = plt.subplots(ncols=3, figsize=(15, 3))
axs[j % 3].plot(arr_target_year, matrix_cnt[:, j], marker='o', color='b', alpha=0.5, label="match num")
axs[j % 3].set(title=player, xlabel='year', ylabel='cnt', ylim=[0, 40])
axs[j % 3].legend()
plt.show()
for j, player in enumerate(arr_target_player):
if j % 3 == 0:
fig, axs = plt.subplots(ncols=3, figsize=(15, 3))
axs[j % 3].plot(arr_target_year, matrix_rate[:, j], marker='o', color='r', alpha=0.5, label="win rate")
axs[j % 3].set(title=player, xlabel='year', ylabel='rate', ylim=[0, 1])
axs[j % 3].legend()
plt.show()
ARR_TARGET_YEAR = np.array(list(range(2005, 2017)))
plot_n_match_win_rate_ts(df_matches, ARR_TARGET_YEAR, ARR_TARGET_PLAYER)

青が年ごとのその選手の試合数、赤が年ごとの勝率です。全ての年で5試合以上はありそうな選手に加えて、錦織を入れています。次にモデルに入力するデータを作成します。試合ごとの「負け・勝ち」のインデックスに加えて、その試合が何年目にあたるかを示す配列を持たせる必要があります。
def get_lw_gy(df, arr_target_year, arr_target_player, dic_target_year, dic_target_player):
"""Get LW data and GY data for input model.
Args:
df (pd.DataFrame): df_matches.
arr_target_year (np.ndarray): Target year list.
arr_target_player (np.ndarray): Target player list.
dic_target_year (dict[int, int]): Dictionary of year to number.
dic_target_player (dict[str, int]): Dictionary of player to number.
Returns:
tuple[np.ndarray, np.ndarray]: LW array and GY array.
"""
LW = []
GY = []
for year in arr_target_year:
for player_a in arr_target_player:
for player_b in arr_target_player:
df_tmp = df[
(df['year'] == year) &
(df['winner_name'] == player_a) &
(df['loser_name'] == player_b)
]
for _ in range(len(df_tmp)):
LW.append([dic_target_player[player_b], dic_target_player[player_a]])
GY.append(dic_target_year[year])
df_tmp = df[
(df['year'] == year) &
(df['winner_name'] == player_b) &
(df['loser_name'] == player_a)
]
for _ in range(len(df_tmp)):
LW.append([dic_target_player[player_a], dic_target_player[player_b]])
GY.append(dic_target_year[year])
LW = np.array(LW, dtype=np.int32)
GY = np.array(GY, dtype=np.int32)
return LW, GY
dic_target_year = get_dict_target(ARR_TARGET_YEAR)
dic_target_player = get_dict_target(ARR_TARGET_PLAYER)
LW, GY = get_lw_gy(
df=df_matches,
arr_target_year=ARR_TARGET_YEAR,
arr_target_player=ARR_TARGET_PLAYER,
dic_target_year=dic_target_year,
dic_target_player=dic_target_player,
)LWは先ほどと同様で「負けプレイヤーインデックス、勝ちプレイヤーインデックス」が試合数分入っており、その試合番号に対応する年がいくつかというリストとしてGYを作成しています。Stanでモデルを実装すると、以下のようになります。
def train_model_ts(data, n_iter, n_chains):
"""Train the model.
Args:
data (dict[str, int or np.ndarray]): Input data.
n_iter (int): The number of iter.
n_chains (int): The number of chains.
Returns:
collections.OrderedDict: Result.
"""
model = """
data {
int N;
int G;
int Y;
int GY[G];
int LW[G, 2];
}
parameters {
ordered[2] performance[G];
matrix[N, Y] mu;
matrix[N, Y] s_mu;
matrix[N, Y] s_pf;
}
model {
for (g in 1:G)
for (i in 1:2)
performance[g, i] ~ normal(mu[LW[g, i], GY[g]], s_pf[LW[g, i], GY[g]]);
for (n in 1:N)
mu[n, 1] ~ normal(0, s_mu[n, 1]);
for (n in 1:N)
for (y in 2:Y)
mu[n, y] ~ normal(mu[n, y-1], s_mu[n, y]);
for (n in 1:N)
s_mu[n] ~ normal(0, 1);
for (n in 1:N)
s_pf[n] ~ gamma(10, 10);
}
"""
fit = pystan.stan(model_code=model, data=data, iter=n_iter, chains=n_chains)
return fit.extract()
data = {
'N': len(dic_target_player),
'G': len(LW),
'Y': len(dic_target_year),
'GY': GY,
'LW': LW,
}
la_ts = train_model_ts(data=data, n_iter=N_ITER, n_chains=N_CHAINS) 
強さmuが選手×年の行列になり、前年の値を今年の平均とする形で連鎖しています。収束の確認については詳細を省きますが、こちらも問題なさそうでした。
結果を可視化します。まずは各選手の強さの時系列推移です。
def plot_posterior_dist_ts(arr_target_year, arr_target_player, la, target_param):
"""Plot posterior distributeion of given latest parameter on each years (time series).
Args:
arr_target_year (np.ndarray): Target year list.
arr_target_player (np.ndarray): Target player list.
la (collections.OrderedDict): Result.
target_param (str): Target latest parameter.
"""
cmap = matplotlib.cm.get_cmap('tab10')
plt.figure(figsize=(15, 7))
for j, player in enumerate(arr_target_player):
samples = la[target_param][:, j, :]
medians = np.median(samples, axis=0)
lower, upper = np.percentile(samples, q=[25.0, 75.0], axis=0)
c = cmap(j)
plt.plot(arr_target_year, medians, marker='o', label=player, color=c)
plt.fill_between(arr_target_year, lower, upper, alpha=0.2, color=c)
plt.xlabel('year')
plt.ylabel(target_param)
plt.legend(loc='lower left', bbox_to_anchor=(1, 0.5))
plt.title("Time series of estimated latent strength")
plt.show()
# Latent strength
plot_posterior_dist_ts(
arr_target_year=ARR_TARGET_YEAR,
arr_target_player=ARR_TARGET_PLAYER,
la=la_ts,
target_param="mu",
)
各選手の強さの年次推移として、次の結果が得られました。2005年頃はフェデラー、ナダルの2強時代であった様子がよくわかります。そして、2008年頃からフェデラー、ナダルに迫る若手、ジョコビッチ、マレー、デル・ポトロなどの強さが上位に推移してきていることもわかります。錦織は2014年に強さが急激に上昇しており、この年に初めて世界ランクトップ10入りしました。デル・ポトロは2009年、初めてグランドスラムに優勝した年に急激に上昇するも、怪我の影響で推移を落としている様子が見えます。マレーがかなりジグザグしていますが、2008年頃からフェデラー、ナダルに勝つようになり、この頃に世界ランク2位まで上り詰めるものの、2014年に怪我で不調となっており、そのような様子がはっきりと見て取れます。
入力したのは年と勝者と敗者だけであり、ランキングも怪我の情報も与えていません。それでも、外部の事実として知られている出来事と対応する形で曲線が動いています。潜在変数を置くモデルが機能しているかどうかは、こうした外部の知識との照合で判断できます。
ひとつ注意点があります。この時系列モデルは年内での強さの比較が主であり、年を跨いだ強さの直接比較はできないのではないかと考えられます。例えば、2007年のフェデラーよりも2008年のナダルの方が強そうな結果は出ていますが、2007年はフェデラーの方が多く勝った、2008年はナダルの方が多く勝ったという結果から推定しているはずなので、直接比較しているわけではないと思われます。異なる年の選手同士は対戦していないため、両者を結ぶ観測が存在しないからです。モデルが出力する数値がどの範囲で意味を持つのかを見極めることは、結果を業務判断に使う際に欠かせません。

続いて、各選手・各年ごとの勝負ムラの事後分布について可視化してみます。
def plot_posterior_dist_ts_ply(arr_target_year, arr_target_player, la, target_param):
"""Plot posterior distributeion of given latest parameter on each years.
Args:
arr_target_year (np.ndarray): Target year list.
arr_target_player (np.ndarray): Target player list.
la (collections.OrderedDict): Result.
target_param (str): Target latest parameter.
"""
cmap = matplotlib.cm.get_cmap('tab10')
for j, player in enumerate(arr_target_player):
if j % 3 == 0:
fig, axs = plt.subplots(ncols=3, figsize=(15, 3))
g = axs[j % 3].violinplot(
la[target_param][:, j, :],
positions=arr_target_year,
showmeans=False,
showextrema=False,
showmedians=False,
)
c = cmap(j%10)
for pc in g['bodies']:
pc.set_facecolor(c)
pc.set_alpha(0.7)
axs[j % 3].set(title=player, xlabel='year', ylabel=target_param)
plt.show()
# Uneven performance
plot_posterior_dist_ts_ply(
arr_target_year=ARR_TARGET_YEAR,
arr_target_player=ARR_TARGET_PLAYER,
la=la_ts,
target_param="s_pf",
)
マレーが、年によって勝負ムラが大きくなるような結果となりました。ワウリンカは、直近である2015年から2017年2月の分析では勝負ムラが大きかったものの、それまでの年で言えば比較的大きくなかったようです。意外だったのは、錦織が、直近の分析と同様、各年においても勝負ムラはあまり大きくない傾向にあり、勝つ相手には勝つ・負ける相手には負けるがハッキリしていそうだという点です。
最後に、各選手・各年ごとの成長の変化量の事後分布を確認します。
# Change of strength
plot_posterior_dist_ts_ply(
arr_target_year=ARR_TARGET_YEAR,
arr_target_player=ARR_TARGET_PLAYER,
la=la_ts,
target_param="s_mu",
)
基本的には強さの時系列モデルの推移と大きさが連動している様子が見られます。初年の強さを0から推定させていることもあり、元から強かったフェデラー、ナダルがいきなり強くなっているように見えてしまうので、この辺りは無視した方が良さそうです。マレーは、フェデラー、ナダルに勝つようになった上向きの強さの変化と、不調による下向きの強さの変化があったことが見て取れます。他も強さの時系列と同様、デル・ポトロは2009年で強さが大きく変化しており、錦織は2014年で強さが大きく変化していることがわかります。
初年の扱いに由来する見かけ上の変化を「無視した方がよい」と判断できるのは、モデルの構造を把握しているからです。出力された図をそのまま解釈すると、フェデラーが2005年に急成長したという誤った読み取りが生まれます。モデルの仮定がどこに影響を及ぼすかを理解したうえで結果を読むという作業は、どのようなベイズモデルにも共通して必要になります。
ここで扱ったモデルは、勝ったか負けたかという情報のみで、それらしい強さの推定を出すことができる点で強力な方法です。テニスという題材は分かりやすさのために選んだものであり、モデルの構造そのものは競技に依存していません。必要なのは「2つ以上の対象が比較され、どちらが上だったかが記録されている」という条件だけです。この条件を満たす業務データは、社内を見渡せばいくつも見つかります。
製品やクリエイティブの比較テストが典型例です。広告バナーやランディングページの案を2つずつ対戦させ、どちらが選ばれたかを記録していけば、それぞれの案の潜在的な訴求力を推定できます。全案を総当たりで比較する必要はなく、部分的な対戦の連鎖があれば、間接的な比較を通じて全体の序列が推定されます。AとBを比べた記録、BとCを比べた記録があれば、AとCの関係も推定できるという性質は、対戦データを扱ううえで実務的に大きな利点です。
提案コンペの勝敗分析にも応用できます。複数の競合と入札で競った記録があれば、自社と各競合の相対的な競争力を推定できます。単純な勝率では、強い競合と当たった案件の敗北も、弱い競合に取りこぼした敗北も、同じ1敗として扱われてしまいます。相手を考慮したモデルであれば、この両者は区別されます。さらに時系列拡張を使えば、自社の競争力が年ごとにどう推移したか、特定の競合が急速に力をつけていないかといった問いにも答えられます。
チーム間・拠点間の実力評価も同じ形に落とし込めます。営業所どうしを同一条件で比較する機会がある場合や、社内のコンテスト形式の評価がある場合には、対戦結果として記録することで、単純な成績合計では見えない実力差が取り出せます。勝負ムラにあたるパラメータは、成果の安定性の指標として読めます。
順位データへの拡張も容易です。参考にした書籍では、アンケートの人気投票などで回答者に1位を回答してもらった場合にも使えると述べられています。その場合は、LWが試合ごとの結果だったのを回答者ごとの結果にして、制約付きパラメータをordered[3]とするイメージで分析が可能でしょう。競馬などのレース結果から馬の強さをモデル化するといった応用も考えられ、1着から5着までの結果を入力してordered[5]で分析すればよいことになります。複数の選択肢に順位を付けてもらう社内アンケートや、候補案のランキング評価も、まったく同じ構造で扱えます。
導入にあたって確認すべき点を整理しておきます。第一に、比較の記録がつながっているかどうかです。対象がいくつかのグループに分断され、グループ間の比較が一切ない場合、グループ間の相対的な強さは推定できません。年をまたいだ比較ができないという先ほどの注意点と同じ理屈です。第二に、比較の条件が揃っているかどうかです。テニスであればサーフェスや大会の格が結果に影響しますが、ここではそれらを無視しました。業務データでも、案件規模や時期の違いが勝敗に効くのであれば、それらを説明変数としてモデルに組み込む拡張を検討することになります。第三に、データ量です。参加者ごとの比較回数が極端に少ない場合、推定の幅が広くなります。ただしこれは欠点ではなく、幅が広いという形で不確かさが正しく報告されるという、この枠組みの利点にあたります。
勝ち負けという記録は、多くの組織で最も素朴な形で残っているデータです。受注と失注、採用と不採用、選択と非選択。こうした2値の記録の背後に連続的な量を置き、その分布を推定するという発想は、既存データの再解釈として実装コストの低い部類に入ります。新たにデータを集める前に、すでに手元にある勝敗の記録から何が読み取れるかを試してみる価値は十分にあります。
『StanとRでベイズ統計モデリング』(松浦健太郎、共立出版):この章のモデルの原型である棋士の強さのモデリングを含め、具体的な問題設定と実装コードが豊富に収録されており、手を動かしながら理解を深められます。
『データ解析のための統計モデリング入門』(久保拓弥、岩波書店):一般化線形モデルから階層ベイズまでを段階的に積み上げる構成で、潜在変数を置く発想がなぜ必要になるのかを丁寧に説明しています。
前章では、対戦データという「勝ち負けの記録しかない」情報から、選手の強さという目に見えない量を推定しました。あの例では、データの生成過程についてこちらが持ち込んだ仮定は比較的軽いものでした。強い方が勝ちやすい、という一般的な想定を確率の言葉に置き換えただけです。
この章で扱うのは、それとは性質の違う問題です。対象となる現象について、すでに数十年にわたって研究され、微分方程式の形で書き下されたモデルが存在する。しかし手元のデータは数十点しかない。こうした状況で、既知の構造とベイズ推定をどう組み合わせるかを、感染症の流行予測という具体例で見ていきます。
題材は新型コロナウイルス(COVID-19)の感染者数です。都道府県別の日次データに対して、感染症の流行を表す古典的なモデルを当てはめ、そのパラメータを最尤推定とベイズ推定のそれぞれで求めます。同じモデル、同じデータに対して二つの推定方法を並べることで、両者が返してくる答えの形がどれだけ違うか、そしてその違いが意思決定にどう効いてくるかが見えてきます。
機械学習が力を発揮するのは、データが大量にあり、その代わりに現象の仕組みがよく分かっていない場面です。どの特徴量がどう効くのかを人間が説明できなくても、十分な量の学習データがあればモデルが対応関係を見つけてくれます。画像認識や需要予測の多くは、この形で成果を出してきました。
一方、実務の現場には逆向きの問題がしばしばあります。データはごく少ない。しかし現象を支配する法則やメカニズムは、物理・化学・疫学・経済学といった分野の蓄積によってすでに分かっている。設備の摩耗、化学反応の進行、新製品の普及、在庫の減り方などがこれにあたります。こうした問題に汎用の機械学習モデルを当てると、学習データが足りないために、まともに外挿できないか、過学習して現実離れした予測を返すかのどちらかになりがちです。
この場合に取るべき道は、データから構造を学ばせることではなく、構造をこちらから与えてしまい、データにはその中の少数のパラメータだけを決めさせることです。方程式の形は既知として固定し、未知なのは方程式に入る係数だけという設定にすれば、推定すべき量は一気に減ります。数十点のデータでも二つや三つのパラメータなら決められます。
ただし、少ないデータで推定したパラメータは当然ながら不確かです。その不確かさを黙って捨てて一つの数値だけを報告すると、予測は自信過剰になります。ここでベイズ推定が効いてきます。パラメータを分布として推定し、その分布を微分方程式に通してやれば、予測そのものが幅を持った形で出てきます。構造モデルとベイズの組み合わせは、この「構造は既知、データは少量、不確実性は無視できない」という条件がそろったときに最も自然な選択になります。
SIRモデルは、感染症の流行の振る舞いを表すための決定論的なモデルです。集団の人口を、感染症の流行とともに「Susceptible(感染可能)」「Infected(感染者)」「Recovered(回復者)」の3つの状態に分けて、それぞれの人数が時間とともにどう移り変わるかを記述します。1927年に登場した歴史のあるモデルでもあります。
主な活用領域はやはり、ある集団の中でインフルエンザや麻疹などの感染者数がどのように流行に乗り、やがて落ち着くのかといった、疫学的な関心が主流です。一方で、一部ではSNS上の話題がどのように広がっていき、やがて落ち着くのかといった調査に応用されることもあるようです。人から人へ伝わって広がり、やがて伝わる相手がいなくなって鎮まる現象であれば、対象が病気でなくても同じ骨格が使えます。
SIRモデルの基本的な微分方程式は以下のとおりです。
\( \frac{dS}{dt} = -\beta \frac{S \cdot I}{N} \)
\( \frac{dI}{dt} = \beta \frac{S \cdot I}{N} - \gamma I \)
\( \frac{dR}{dt} = \gamma I \)
ここで、\( S \)は感染可能人口、\( I \)は感染者数、\( R \)は回復者数、\( N \)は総人口、\( \beta \)は感染率、\( \gamma \)は除去率(回復率)です。感染率\( \beta \)は、感染可能な人が感染者と接触した際にどの程度の確率で感染するかを表すパラメータであり、除去率\( \gamma \)は感染者がどの程度の速度で回復(もしくは除去)されるかを表すパラメータです。
式が3本並んでいますが、読み方は難しくありません。1本目は、感染可能な人が減っていく速さを表します。減り方は、まだ感染していない人の数\( S \)と、いま感染している人の数\( I \)の掛け算に比例します。出会いの機会が両者の人数の積で決まるという考え方です。2本目は感染者数の増減で、新しく感染した分が入ってきて、回復した分が出ていきます。3本目は回復者が増えていく速さで、いまの感染者数に比例します。
重要なのは、この3本の式に登場する未知の数が\( \beta \)と\( \gamma \)の二つしかないことです。人口\( N \)は統計から与えられ、初期状態は観測データの最初の日から取れます。つまり、何十日ぶんの時系列データがあっても、そこから決めるべき数はたった二つです。データ数に対して推定すべきパラメータが極端に少ないこの比率が、少量データでも推定が成り立つ理由になります。
もう一つ、この二つのパラメータが揃うと、流行の性質を要約する量が計算できます。感染者1人が平均して何人にうつすかを表す基本再生産数は\( R_0 = \beta / \gamma \)で与えられ、これが1を超えていれば流行は拡大し、1を下回れば自然に収束に向かいます。また、\( 1 / \gamma \)は感染者が回復するまでの平均日数にあたります。パラメータそのものは抽象的ですが、こうした形に直すと現場の感覚と突き合わせられます。
都道府県別のコロナ感染者数のデータは以下を利用しました。(2020/04/23時点)
https://github.com/kaz-ogiwara/covid19
実際に読み込むデータはこのリポジトリのうちのprefectures.csvです。都道府県別に、感染者数、回復者数、死亡者数が時系列で格納されています。このデータで、I=感染者数、R=回復者数+死亡者数として、流行を予測します。
死亡者を回復者と同じ区分に入れるのは、直感的には抵抗があるかもしれません。しかしSIRモデルにおける\( R \)は「回復した人」ではなく「もう他人にうつさなくなった人」を意味します。この区分が担っているのは感染連鎖からの離脱であり、その意味では回復も死亡も同じ扱いになります。モデルの各区分が現実の何に対応するのかを、こうして一つずつ決めていく作業が、構造モデルを実務に当てはめるときの中心的な仕事です。
初期状態は、データの最初の日の感染者数と回復者数、そして総人口からそれらを引いた残りとして与えます。東京都であれば総人口を1400万人として、\( S_0 = N - I_0 - R_0 \)を感染可能人口の出発点にします。
SIRモデルのパラメータの推定は、オーソドックスには最尤推定が用いられるようです。まずはコロナ感染者数のデータから、SIRモデルのパラメータを最尤推定で求め、今後の流行の様子を推定することにします。
やることは単純です。適当な\( \beta \)と\( \gamma \)を仮に置いて微分方程式を数値的に解き、そこから出てくる感染者数の曲線と、実際に観測された感染者数のずれを測る。そのずれが最も小さくなる\( \beta \)と\( \gamma \)を探す。この探索を最適化アルゴリズムに任せます。
SIRモデルおよび尤度関数は、scipyのodeintやminimizeを使って、以下のように実装できます。
先にお断りしておくと、本章の後半で使うStanのコードと実行結果は、PyStan 2系と当時のStan構文で実装したときの実測です。現行の環境で動かす場合は、次のように読み替えてください。pystan.StanModel()やfit.extract()はPyStan 2系のAPIなので、PyStan 3ではimport stan、stan.build()、posterior.sample()を使います。第5章で標準として紹介したCmdStanPyに揃えるのも良い選択です。Stanコード側も、real[] yのような旧配列構文はStan 2.33以降エラーになるためarray[] real yの形へ、常微分方程式の求解は非推奨となったintegrate_odeやintegrate_ode_rk45から現行のode_rk45へ、それぞれ書き換える必要があります。
from numpy import inf
import numpy as np
from scipy.optimize import minimize
from scipy.integrate import odeint
def sir(y, t, beta, gamma):
dydt = np.zeros(3)
dydt[0] = - beta * y[0] * y[1] / (y[0] + y[1] + y[2]) # dS/dt
dydt[1] = beta * y[0] * y[1] / (y[0] + y[1] + y[2]) - gamma * y[1] # dI/dt
dydt[2] = gamma * y[1] # dR/dt
return dydt
def estimate(target_pred_var, init_state, beta, gamma):
sol = odeint(sir, init_state, np.arange(0, len(target_pred_var)), args=(beta, gamma))
return sol
def loss(x, init_state, target_pred_var):
beta, gamma = x[0], x[1]
sol = estimate(target_pred_var, init_state, beta, gamma)
return np.sum((sol[:, 1] - target_pred_var) ** 2)
def optimize(init_state, target_pred_var):
result = minimize(loss, x0=[0.001, 0.001], args=(init_state, target_pred_var),
method="Nelder-Mead")
return result
def prediction(result, n_pred, init_state):
beta = result.x[0]
gamma = result.x[1]
sol = estimate(np.zeros(n_pred), init_state, beta, gamma)
return sol
ここでは、SIRモデルの微分方程式をodeintで数値的に解き、感染者数の時系列データとの二乗誤差を最小化することで、感染率\( \beta \)と除去率\( \gamma \)を推定しています。minimizeに指定しているNelder-Meadは、勾配を使わずに探索する方法です。微分方程式を数値的に解いた結果に対して勾配を求めるのは手間がかかるため、こうした場面ではよく使われます。
たとえば、東京都の感染者数のデータを使い、パラメータの初期値を設定して、以下のように最適化を実行すると、パラメータを推定することができます。
df_tmp = df.reset_index()
df_tmp['dt'] = df_tmp['dt'].astype('str')
df_tmp['dt2'] = df_tmp['dt'].apply(lambda x: dt.strptime(x, '%Y%m%d'))
target_pred_var = df_tmp['infected'].values
target_pred_var_r = df_tmp['recovered'].values
I = target_pred_var
R = target_pred_var_r
N = 14000000
S = N - I - R
init_state = [S[0], I[0], R[0]]
result = optimize(init_state, target_pred_var)
print(result)
推定結果の例は以下のとおりです。
final_simplex: (array([[1.98561299e-01, 1.39422106e-01],
[1.98561055e-01, 1.39421706e-01],
[1.98563439e-01, 1.39424393e-01]]),
array([106399.61884819, 106399.61886498, 106399.61935616]))
fun: 106399.61884819488
message: 'Optimization terminated successfully.'
nit: 89
nfev: 178
status: 0
success: True
x: array([0.19856129, 0.13942211])
x[0]が感染率、x[1]が除去率です。感染率が約0.1986、除去率が約0.1394という値が得られました。
この二つの数値を、先ほどの解釈に当てはめてみます。基本再生産数は\( R_0 = \beta / \gamma = 0.19856129 / 0.13942211 \)を計算して約1.42となります。感染者1人が平均して1.42人にうつしている、という読み方です。1を超えているので、この時点では流行はまだ拡大局面にあります。また\( 1 / \gamma = 1 / 0.13942211 \)は約7.2となり、感染者が感染連鎖から抜けるまでの平均日数はおよそ7日という計算になります。
実際のデータおよび推定されたパラメータを使って、その後の流行(感染者数の推移)を可視化すると、以下のようになりました。
n_pred = 50
t_max = len(target_pred_var) + n_pred
dt = np.arange(0, t_max)
beta = result.x[0]
gamma = result.x[1]
dates = data.index.tolist()
for i in range(n_pred):
d = d + datetime.timedelta(days=1)
dates.append(d)
sol = odeint(sir, init_state, dt,
args=(beta, gamma))
plt.figure(figsize=(10,6))
plt.plot(dates, sol[:,0] / N, 'b', label='Susceptible (estimation)')
plt.plot(dates, sol[:,1] / N, 'r', label='Infected (estimation)')
plt.plot(dates, sol[:,2] / N, 'g', label='Recovered (estimation)')
ax1.plot_date(dates[:len(I)], I / N, 'or', label='Infected (observation)')
ax1.plot_date(dates[:len(R)], R / N, 'og', label='Recovered (observation)')
plt.legend()
plt.xlabel('Date')
plt.ylabel('Population')
plt.title('Prediction of Covid-19 epidemic in ' + target_pref_str)
plt.show()

推定結果によれば、ゴールデンウィークを開けてすぐには感染者数の上昇はピークに達し、その後緩やかに流行は収まっていくという結果になっています。
グラフは1本の滑らかな曲線です。ピークの日付も、そのときの感染者数も、収束していく速さも、すべて一つに定まっています。見た目は明快で、報告資料には載せやすい形をしています。
しかしこの明快さは、推定の中身から来ているのではなく、推定の出力形式から来ています。最尤推定が返したのは\( \beta \)と\( \gamma \)の値それぞれ一つずつであり、その値がどれくらい確からしいかという情報は、この出力のどこにも入っていません。少し違う\( \beta \)を使っていたらピークが1週間ずれていたかもしれない、といった可能性はグラフから消えています。データが数十点しかない状況で、消してよい情報ではありません。
次に、ベイズでSIRモデルを表現してみて、感染率、除去率、予測の事後分布を推定してみます。
以下の論文が似たようなことに挑戦していたので、これを参考にしました。
Contemporary statistical inference for infectious disease models using Stan: https://arxiv.org/abs/1903.00423
Stanではintegrate_ode関数で微分方程式の計算を表すことができるようです。これに則って、感染者数、除去者数がポアソン分布に従って発生するとして、以下のようなモデルを書いてみました。
model_code = """
functions {
real[] sir(real t,
real[] y,
real[] theta,
real[] x_r,
int[] x_i) {
real dydt[3];
dydt[1] = - theta[1] * y[1] * y[2] / (y[1] + y[2] + y[3]);
dydt[2] = theta[1] * y[1] * y[2] / (y[1] + y[2] + y[3]) - theta[2] * y[2];
dydt[3] = theta[2] * y[2];
return dydt;
}
}
data {
int T;
real y0[3];
real t0;
real ts[T];
int I[T];
int R[T];
}
transformed data {
real x_r[0];
int x_i[0];
}
parameters {
real<lower=0> beta;
real<lower=0> gamma;
}
transformed parameters {
real y_hat[T, 3];
y_hat = integrate_ode_rk45(sir, y0, t0, ts, {beta, gamma}, x_r, x_i);
}
model {
beta ~ normal(0, 10);
gamma ~ normal(0, 10);
for (t in 1:T) {
I[t] ~ poisson(max({y_hat[t, 2], 0.0}));
R[t] ~ poisson(max({y_hat[t, 3], 0.0}));
}
}
generated quantities {
real y_pred[T, 3];
y_pred = integrate_ode_rk45(sir, y0, t0, ts, {beta, gamma}, x_r, x_i);
}
"""
このモデルでは、SIRの微分方程式をfunctionsブロック内で定義し、integrate_ode_rk45でルンゲ・クッタ法による数値解を求めています。感染率\( \beta \)と除去率\( \gamma \)に正規分布の事前分布を設定し、観測データ(感染者数\( I \)と回復者数\( R \))がポアソン分布に従うとしてモデルを構成しています。
このコードの読みどころは、微分方程式を解く処理がモデルの内部に埋め込まれている点です。transformed parametersブロックは、サンプリングの途中でパラメータの値が変わるたびに評価されます。つまりMCMCが\( \beta \)と\( \gamma \)の候補を一組提案するたびに、その候補で微分方程式が解き直され、得られた曲線とデータとの当てはまりが評価されるという流れになっています。ドメイン知識である微分方程式が、推定のループの中に部品として組み込まれているわけです。
観測モデルにポアソン分布を選んでいるのも意味があります。感染者数は0以上の整数であり、正規分布のように負の値を取り得る分布では表現が不自然になります。カウントデータにはカウントデータ用の分布を当てるという原則に従っています。max({y_hat[t, 2], 0.0})で下限を0にしているのは、数値計算の誤差でごくわずかに負になった値がポアソン分布の平均として渡らないようにするための処置です。
事前分布は\( \beta \)、\( \gamma \)ともに正規分布の\( N(0, 10) \)で、下限0の制約がかかっています。標準偏差10という設定は、感染率や除去率として現実的に取り得る範囲(せいぜい0から1程度)に比べればかなり広く、事前分布が結果をほとんど左右しない弱い設定です。ドメイン知識で構造は強く縛る一方、パラメータの値そのものについてはデータに語らせる、という役割分担になっています。
これをMCMCで解いて、y_hat、beta、gammaから予測、感染率、除去率のサンプリング結果を得ます。たとえば、東京・大阪・福岡・愛知の4つの都道府県で、それぞれベイズ推定を行い、得られたパラメータで今後の流行予測を可視化してみます。
fig_save = plt.subplots(nrows=2, ncols=2, figsize=(20, 15))
axs = fig_save[1].flatten()
for i, target_pref_no in enumerate(['Tokyo', 'Osaka', 'Aichi', 'Fukuoka']):
# データ準備
t_obs = np.arange(1, len(I)+1).tolist()
t_obs_pred = np.arange(1, len(I)+n_pred+1).tolist()
stan_data = {
'T': len(t_obs),
'y0': [S[0], I[0], R[0]],
't0': 0,
'ts': t_obs,
'I': I.astype(int).tolist(),
'R': R.astype(int).tolist(),
}
# コンパイルとサンプリング
sm = pystan.StanModel(model_code=model_code)
fit = sm.sampling(data=stan_data, iter=2000, chains=4, warmup=500, seed=42)
# 予測結果の取得
y_hat = fit.extract()['y_hat']
beta_samples = fit.extract()['beta']
gamma_samples = fit.extract()['gamma']
4本の連鎖をそれぞれ2000回まわし、最初の500回を捨てています。残った6000個ぶんのサンプルは、\( \beta \)と\( \gamma \)の事後分布から取り出した値の集まりです。そして重要なのは、y_hatにはそのサンプル1つ1つに対応する時系列の曲線が入っているという点です。パラメータの組が6000通りあれば、感染者数の予測曲線も6000本あります。
得られたサンプリング結果を用いて、以下のように流行予測を可視化するコードを作成しました。
# 予測用のデータ
t_obs = np.arange(1, len(I)+1).tolist()
n_pred = 50
t_max = len(I) + n_pred
t_all = np.arange(1, t_max+1).tolist()
# 予測の再計算
stan_data_pred = stan_data.copy()
stan_data_pred['T'] = len(t_all)
stan_data_pred['ts'] = t_all
# 可視化
y_hat = fit.extract()['y_hat']
median = np.median(y_hat, axis=0)
lower = np.percentile(y_hat, 2.5, axis=0)
upper = np.percentile(y_hat, 97.5, axis=0)
ax = axs[i]
# 実データ
ax.plot_date(dates[:len(I)], I, 'or', label='Infected (observation)')
ax.plot_date(dates[:len(R)], R, 'og', label='Recovered (observation)')
# 推定結果(中央値と95%信用区間)
ax.plot(dates, median[:, 1], color='r', alpha=0.7, linestyle='dashed', label='Infected (estimation)')
ax.fill_between(dates, lower[:, 1], upper[:, 1], color='r', alpha=0.1)
ax.plot(dates, median[:, 2], color='g', alpha=0.7, linestyle='dashed', label='Recovered (estimation)')
ax.fill_between(dates, lower[:, 2], upper[:, 2], color='g', alpha=0.1)
ax.legend()
ax.set_xlabel('Date')
ax.set_ylabel('Population')
ax.set_title('Prediction of Covid-19 epidemic in ' + target_pref_str)
plt.tight_layout()
plt.show()
ここで作成しているstan_data_predは、予測したい期間までtsを伸ばしたデータですが、この定義を作るだけでは予測期間は伸びません。fitから取り出すy_hatは、あくまで最初にサンプリングしたときの観測期間ぶんの長さのままだからです。将来の50日ぶんまで描くには、予測用の期間を受け取るT_predやts_predをStanモデル側に持たせたうえで、そのデータでgenerated quantitiesを再実行する必要があります。CmdStanPyであればgenerate_quantitiesにあたる手順です。
可視化の部分に注目してください。np.medianで各時点の中央値を取り、np.percentileで2.5パーセンタイルと97.5パーセンタイルを取っています。6000本の曲線を各時点で縦に切り、その分布の真ん中と両端を拾っているわけです。破線が中央値、薄く塗られた帯が95%信用区間になります。この帯こそが、最尤推定のグラフからは失われていた情報です。

東京や大阪に関しては、データ数も多く、そもそも報告の結果も滑らかになっていたためか、とてもフィットしているように見えます。最尤推定の時の結果と同様に、東京および大阪は、ゴールデンウィークを開けてすぐの頃にピークに達した後に、徐々に収束に向かうような形になりました。
愛知、福岡に関しては、そもそもあまりフィットしていなさそうです。このモデル自体に、表現できる振る舞いに強い条件がついていそうな印象を受けます。SIRモデルが描ける曲線は「立ち上がって、一つの山を作って、下がる」という形に限られており、報告体制の変化や複数回の流行の波といった動きは、そもそも表現の範囲に入っていません。
この当てはまりの悪さが、帯の広さとして目に見える形で出てくるのがベイズ推定の利点です。最尤推定であれば、当てはまりが悪くても曲線は1本きれいに引かれてしまい、その1本を見ただけでは信用してよいかどうかの判断がつきません。帯が広いという事実は、そのまま「この予測に基づいて意思決定をするのは危うい」という警告として機能します。
今度はデータが取れている都道府県すべてに対してベイズ推定を実施し、感染率・除去率の事後分布を可視化します。都道府県ごとに箱ひげ図を並べると、値の大小だけでなく、推定のばらつきの大小も同時に読み取れます。


感染率が高いと推定されている都道府県は、事後分布の幅も広い傾向にあります。調べてみるとデータ数も少し少なめで、信頼性の観点では懸念がありそうです。データが少なければ推定は不確かになるという当たり前の関係が、箱の長さという形で自動的に表現されている点に注目してください。この対応関係を人間が別途チェックする必要がありません。
逆に、人数が多い都道府県の感染率が低いという傾向が見られます。人口そのものを説明変数として入力しているわけではないのですが、分母となる人口規模を通じて、そのあたりの事情を反映した推定になっているのかもしれません。
除去率に関しては、データを見て感じたことと整合していました。東京は、人数や感染者数に対してあまり除去されていない状態であり、結果としてパラメータも相対的に低めに推定されたと考えられます。
ここで得られている情報の使い方には、二つの段階があります。一つは値そのもので、地域ごとの感染率や除去率の違いを比べるという使い方です。もう一つは幅の使い方で、どの地域の推定が信用できてどの地域が信用できないかを判定するという使い方です。後者は最尤推定では得られません。全都道府県について1点ずつ推定値が並んだ表を見ても、そのうちどれが3日分のデータから出た数字でどれが40日分から出た数字なのかは表に書かれていないからです。
同じSIRモデル、同じデータを使って、最尤推定とベイズ推定を並べました。数値の中心はおおむね似た結論を示しています。東京と大阪はゴールデンウィーク明けにピークを迎えて収束に向かう、という点で両者は一致していました。違うのは、答えとして返ってくる情報の形です。
最尤推定が返すのは\( \beta = 0.19856129 \)、\( \gamma = 0.13942211 \)という2つの数値です。この2つを微分方程式に入れると、予測曲線は1本に決まります。ピークの日は1日に決まり、そのときの感染者数も1つの数に決まります。
ベイズ推定が返すのは、\( \beta \)と\( \gamma \)の組み合わせが6000通り入ったサンプルの集合です。この6000通りを微分方程式に入れると、予測曲線が6000本出てきます。ピークの日は6000個の日付の分布になり、ピーク時の感染者数も6000個の値の分布になります。この分布から生まれるのが事後予測分布であり、グラフに描いた帯はその要約です。
この違いが意思決定にどう効くかを、病床の確保という判断で考えてみます。最尤推定の答えから読めるのは「ピーク時の感染者数はこれだけ」という1つの数です。この数に合わせて病床を用意すれば、予測が当たっていれば足ります。しかし予測が外れたときにどれだけ足りなくなるのか、どれくらいの確率で外れるのかは、この答えの中に情報がありません。判断者は「予測より多めに見ておく」という調整を自分の勘で行うことになります。何割増しにするかの根拠はどこにもありません。
ベイズ推定の答えからは違う問いが立てられます。ピーク時感染者数の95パーセンタイルはいくつか。用意した病床数を超える確率は何パーセントか。上振れした場合に不足する人数の期待値はどれくらいか。いずれも6000本の予測曲線を数えれば計算できます。ここまで来ると、「多めに見る」という判断が勘ではなく、超過確率を何パーセント以下に抑えたいという方針の表明になります。方針は関係者の間で議論でき、後から検証もできます。
企業の意思決定でも構図は同じです。ある設備が故障するのはいつか、ある製品の需要がピークを迎えるのはいつか、在庫が何日で捌けるか。こうした問いに1つの数値で答えると、受け取った側はそれを確定した事実として扱いがちです。予測が外れたときの備えは、予測を出した側でも受け取った側でも設計されていません。幅を添えて答えれば、備えの厚みを幅から逆算できます。
もう一つ、幅は「わからない」を明示する手段でもあります。愛知と福岡の予測は帯が広く、当てはまりも良くありませんでした。この結果は、モデルがこの地域では機能していないという情報を返しています。1本の曲線しか出さない方法では、機能していないことがグラフの見た目からは判別できず、他の地域と同じ確からしさで扱われる恐れがあります。予測を出さないほうがよい場面を自動的に検出できるという点は、実務では見落とされがちですが大きな価値があります。
ここまでの結果を実務に持ち込むときに、押さえておくべき限界がいくつかあります。
第一に、幅が正しいのはモデルが正しい場合に限られます。信用区間は「このSIRモデルが現実を表しているという前提のもとで、パラメータがどれくらい不確かか」を測ったものです。SIRモデルが表現できない現象、たとえば人々の行動変容による感染率の途中変化、複数の波の到来、ワクチン接種の効果などは、そもそもモデルの外にあります。モデルが間違っている分の不確実性は、この帯には入っていません。帯が狭いことは、予測が当たることの保証にはなりません。
第二に、SIRモデルは\( \beta \)と\( \gamma \)が期間を通じて一定であることを仮定しています。実際には緊急事態宣言のような介入によって接触の頻度は大きく変わります。この点に対応するには、\( \beta \)を時間の関数にする、区分ごとに別の値を推定する、といったモデルの拡張が必要になります。拡張自体はベイズの枠組みの中で自然に行えますが、パラメータが増えれば少ないデータで推定するという当初の利点は薄れていきます。構造の複雑さとデータ量のつり合いを、どこで取るかという判断が常に伴います。
第三に、微分方程式を含むモデルのMCMCは計算負荷が高くなります。サンプリングの1ステップごとに常微分方程式を数値的に解くため、通常の回帰モデルと比べて実行時間は桁違いに長くなります。都道府県ごとにモデルをコンパイルし直す実装であればなおさらです。実務で使う場合は、計算時間と更新頻度のつり合いを設計段階で考えておく必要があります。
第四に、データそのものの質です。感染者数は検査数に依存し、報告日のずれもあります。回復者数の定義は自治体によって運用が異なることもあります。モデルとベイズ推定はデータをそのまま受け取って処理するため、報告の癖はパラメータの推定値に吸収されます。愛知や福岡での当てはまりの悪さも、モデルの限界だけでなく報告データの性質を反映している可能性があります。
この章の題材は感染症でしたが、同じ骨格が使える問題は事業の中に数多くあります。共通する条件は3つです。現象の仕組みが方程式や既知の法則で書ける。手元のデータが少ない。予測を外したときのコストが大きく、不確実性を明示する価値がある。
設備の劣化予測がその一つです。摩耗や疲労、腐食の進行には、材料工学で確立された経時変化の法則があります。故障は稀にしか起きないため、機械学習に学習させるほどのデータは集まりません。しかし劣化速度を表すパラメータが1つか2つであれば、限られた点検記録から推定できます。ベイズで推定すれば「あと何か月で交換基準に達するか」が分布として得られ、交換時期を決めるときに「基準到達確率が5%を超える時点」といった運用ルールが設計できます。1点の予測寿命だけでは、余裕を何か月取るかの根拠が作れません。
新製品の需要の立ち上がりも同じ形をしています。普及の広がり方には、口コミによる伝播と、広告など外部からの働きかけの両方が効きます。感染症と同じく、まだ買っていない人と既に買った人の相互作用として書けるため、方程式の骨格はSIRモデルに近いものになります。発売直後は数週間ぶんのデータしかありませんが、パラメータが少なければ推定は可能です。ここでも重要なのは幅です。生産計画を立てるときに必要なのは、需要のピークがいつかという1点ではなく、上振れしたときにどこまで行き得るかという上限側の見積もりだからです。
在庫の消化も構造が既知の問題です。入荷と出荷、廃棄という流れの収支は明確に書けます。未知なのは消化速度を表す係数だけです。新規取り扱い商品や季節商品のように履歴の短い品目では、汎用の需要予測モデルは学習データ不足で機能しません。構造を固定して係数だけをベイズで推定すれば、消化しきる日付の分布が得られ、値下げのタイミングや追加発注の判断に確率の形で答えを出せます。
これらに共通するのは、モデルを作る作業の中身が「データから何かを学ばせる」ことではなく、「現象をどういう箱と流れで表すかを決める」ことになる点です。SIRモデルにおいて死亡者を回復者と同じ区分に入れると決めたように、業務の実態とモデルの部品を対応づける作業が中心になります。これは分析担当者だけでは決められず、現場の知識を持つ人との共同作業になります。逆に言えば、現場の知識をモデルに組み込む正規の入り口がここにある、ということでもあります。
汎用の機械学習モデルには、この入り口がありません。特徴量を追加するという形でしか知識を入れられず、しかもその知識が正しく使われたかどうかは学習結果からは確認しにくいままです。構造モデルであれば、方程式を読めば何を仮定したかが明示されており、現場の人が見て違和感を指摘できます。説明可能性という観点でも、この違いは小さくありません。
そしてベイズ推定は、この構造モデルに不確実性の扱いを付け加えます。構造だけでは、パラメータが1つに決まってしまい、少ないデータで決めた値がどれだけ危ういかが伝わりません。ベイズで推定すれば、データの少なさは自動的に幅の広さとして表れ、予測を使う側がその幅を見て判断の余裕を設計できます。データが少ない領域でこそ、この組み合わせの価値が出ます。
次章では、時間とともに変化する量そのものをベイズで扱う方法に進みます。この章では方程式の形をこちらから与えましたが、状態空間モデルでは、水準やトレンドといった目に見えない状態が時間とともにどう動くかを確率的に記述します。構造の与え方がより柔軟になり、扱える現象の範囲が広がります。
『StanとRでベイズ統計モデリング』(松浦健太郎、共立出版):Stanでモデルを書くときの作法が、当てはまらなかったときの対処まで含めて丁寧に説明されています。微分方程式を含むモデルのように収束が難しいケースでの診断の考え方は、この章の内容を実際に動かすときに役立ちます。
『感染症の数理モデル』(稲葉寿 編著、培風館):SIRモデルとその拡張を数理の側から体系的に扱った和書です。この章では2つのパラメータのモデルだけを扱いましたが、潜伏期間や年齢構造を入れた拡張がどう組み立てられるかを知ると、構造モデルの設計の幅が広がります。
ここまでの章では、データが互いに独立に得られる場面を主に扱ってきました。ある顧客の反応と別の顧客の反応、ある地域の件数と別の地域の件数、それらは順番を入れ替えても意味が変わりません。ところが実務で扱うデータの多くは時間の順に並んでいて、順番を入れ替えると意味が壊れます。日次の売上、週次の在庫、月次の解約率、時間ごとのアクセス数。これらは前の時点の値と次の時点の値が無関係ではなく、昨日高ければ今日も高い傾向があります。この構造を無視して平均や分散だけを見ると、実態から離れた結論になります。
この章で扱うのは状態空間モデルです。観測された数字の背後に、直接は見えない「状態」があると考え、その状態が時間とともにどう動くかを確率的に書き下す枠組みです。ベイズ統計と組み合わせると、状態の推定値が一つの線ではなく幅を持った帯として得られます。この帯こそが、経営判断に必要な情報です。
なお、時系列予測の手法そのものを網羅的に比較する内容は、当社の別のコラム「ビジネスデータの予測に効く時系列分析、基礎から実務まで」で扱っています。この章は予測手法のカタログではなく、不確実性を幅で持ちながら時系列の構造を読むというベイズならではの視点に絞って説明します。
日次の売上データを思い浮かべてください。ある日は120万円、翌日は95万円、その翌日は135万円だったとします。この上下は何を意味しているでしょうか。事業の実力が本当に日ごとに大きく変わっているわけではないはずです。たまたま大口の注文が入った、たまたま雨が降った、たまたま担当者が休んだ。そうした偶然の要因が、本来の実力の上に乗って観測値を揺らしています。
状態空間モデルの出発点は、この「本来の実力」と「観測された数字」を分けて考えることです。本来の実力にあたるものを状態と呼びます。状態は直接は測れません。測れるのは、状態にノイズが乗った観測値だけです。そこで、モデルを二階建てにします。
一階部分が観測方程式です。時点 \( t \) で観測される値 \( y_t \) は、その時点の状態 \( \mu_t \) に観測ノイズ \( \varepsilon_t \) が加わったものだと書きます。
\( y_t = \mu_t + \varepsilon_t, \quad \varepsilon_t \sim N(0, \sigma_{\text{obs}}^2) \)
二階部分が状態方程式です。状態そのものが時間とともにどう動くかを書きます。最も単純な形は、今日の状態は昨日の状態に少しの変動が加わったものだ、という書き方です。
\( \mu_t = \mu_{t-1} + \eta_t, \quad \eta_t \sim N(0, \sigma_{\text{state}}^2) \)
この二本の式が意味しているのは、観測値の揺れを二種類に分けたということです。一つは状態そのものが変化したことによる揺れ、もう一つは状態は変わっていないのに測定の過程で乗った揺れです。前者は事業の実力が動いたことを示すので、経営として反応すべき変化です。後者は反応すべきでない雑音です。両者を分離できるかどうかが、日次のダッシュボードを見て一喜一憂する運用と、意味のある変化にだけ手を打つ運用の分かれ目になります。
ここでベイズの枠組みが効いてきます。分離の程度は、二つの分散 \( \sigma_{\text{obs}}^2 \) と \( \sigma_{\text{state}}^2 \) の比で決まります。この比は事前に分かるものではなく、データから推定するしかありません。ベイズ推定では両方の分散に事前分布を置き、データを見た後の事後分布として、比そのものの不確実性も含めて答えを得ます。「状態のブレは観測ノイズのおよそ三分の一だが、その見積もり自体に幅がある」という形で結果が出るわけです。

いま書いた二本の式からなるモデルをローカルレベルモデルと呼びます。局所水準モデルとも訳されます。状態空間モデルの中で最も単純な形であり、時系列の水準(レベル)が時間とともにゆっくり漂うことだけを表現しています。
このモデルの挙動は、二つの分散の比で大きく変わります。この比を \( q = \sigma_{\text{state}}^2 / \sigma_{\text{obs}}^2 \) と書き、信号対雑音比と呼ぶことがあります。
\( q \) が小さいとき、つまり状態のブレが観測ノイズに比べて小さいとき、モデルは「実力はほとんど変わっていない。見えている上下はほぼ雑音である」と解釈します。推定される状態の線はほぼ水平に近くなり、長期平均に近い値を返します。逆に \( q \) が大きいとき、モデルは「実力そのものが動いている」と解釈し、推定される状態は観測値をかなり忠実に追いかけます。極端に \( q \) が大きい場合、状態の推定値は観測値そのものに近づき、平滑化の効果は失われます。
実務でよく使われる指数平滑法(直近の値に大きな重みを置き、過去にさかのぼるほど重みを指数的に減らして平均を取る手法)は、このローカルレベルモデルの特別な場合として導けます。指数平滑法の平滑化パラメータは \( q \) の関数として表現できます。つまり、現場で経験的に「平滑化係数は0.2くらいがちょうどよい」と調整してきた作業は、状態空間モデルの言葉では「観測ノイズに対する状態変動の大きさをこう見積もっている」という仮定の表明にほかなりません。ベイズの枠組みで書き直すと、この係数を勘で決める代わりにデータから推定でき、しかも推定値の不確実性まで得られます。
単純なモデルですが、応用範囲は広いです。ウェブサイトの日次コンバージョン率、コールセンターの日次応答率、工場の日次歩留まり。いずれも「本来の水準はゆっくり動いているはずだが、日々の観測は大きく揺れる」という性質を持ちます。こうしたデータに対して、生の折れ線グラフではなく状態の推定値とその信用区間を並べて示すだけで、会議の議論の質が変わります。信用区間が重なっている二つの時点について「先週より下がった」という議論をしても、意味のある差ではないと分かるからです。
ローカルレベルモデルは水準の漂いを表しますが、方向性のある動きは表現しません。売上が毎月着実に伸びている局面では、水準だけを状態にしたモデルは常に後追いになり、予測は実績より低めに出続けます。
そこで、状態を二つに増やします。水準 \( \mu_t \) に加えて、傾き(トレンド)\( \delta_t \) を状態として持たせるのです。
\( \mu_t = \mu_{t-1} + \delta_{t-1} + \eta_t \)
\( \delta_t = \delta_{t-1} + \zeta_t \)
一本目は、今日の水準は昨日の水準に昨日の傾きを足したものだ、という式です。二本目は、傾き自体もゆっくり変化してよい、という式です。傾きが定数なら二本目の分散をゼロにすればよく、その場合は直線的な成長を仮定したことになります。傾きの分散を大きく取れば、成長が加速したり減速したりする局面にも追随します。
ここでも、傾きの分散という一つのパラメータに、事業についての仮定が凝縮されている点が重要です。「成長率は当面変わらないはずだ」という見立てと「成長率自体が月ごとに変わりうる」という見立ては、経営判断としては大きく違います。状態空間モデルはその違いを分散の大きさとして明示的に書かせます。そして、どちらの見立てがデータと整合するかを、事後分布の形で判定できます。
実務上の注意として、トレンド成分を入れると長期予測が発散しやすくなります。推定された傾きがそのまま将来にわたって続くと仮定するため、数十期先まで外挿すると非現実的な値になります。これを抑えるために、傾きに減衰率を掛けて \( \delta_t = \phi \delta_{t-1} + \zeta_t \) とし、\( \phi \) を1より小さい値に推定させる形がよく使われます。将来に向かって成長が鈍化する、という前提をモデルに組み込むわけです。
ビジネスデータの多くには周期があります。曜日による変動、月末月初の変動、四半期の変動、年間の繁閑。これらを無視すると、水準の推定が周期のせいで振り回されます。
季節成分 \( \gamma_t \) を三つ目の状態として加えます。観測方程式は次のようになります。
\( y_t = \mu_t + \gamma_t + \varepsilon_t \)
季節成分の状態方程式は、周期の長さを \( s \) として、一周期分を足し合わせるとおよそゼロになるという制約で書きます。
\( \gamma_t = -\sum_{j=1}^{s-1} \gamma_{t-j} + \omega_t \)
この形にする理由は、水準と季節を分離するためです。もし季節成分の合計がゼロという制約を置かなければ、季節成分全体を一律に持ち上げて水準を同じだけ下げる、という組み合わせが無数に存在してしまい、どちらがどれだけ寄与しているかを決められなくなります。合計をゼロに縛ることで、季節成分は「平均からのずれ」という意味を持ち、水準は「季節を均した実力」という意味を持ちます。
\( \omega_t \) の分散をゼロに近づければ季節パターンは年を通じて固定され、大きく取れば季節パターン自体が年々変化することを許します。小売業で「近年は年末商戦の山が以前ほど高くない」といった変化が起きている場合、後者の設定でその変化を捉えられます。
曜日効果と年周期のように、複数の周期が重なる場合は季節成分を複数持たせます。日次データであれば週周期(\( s = 7 \))と年周期を同時に扱うことになりますが、年周期を \( s = 365 \) の形式で書くと状態の次元が非常に大きくなるため、実務では三角関数の重ね合わせ(フーリエ項)で近似することが多いです。少数の正弦波と余弦波の組み合わせで滑らかな年周期を表現すれば、状態の数を数個に抑えられます。
ここまでで、水準、トレンド、季節という三つの成分を持つモデルができました。観測された系列は、この三つの和にノイズが乗ったものだと解釈されます。ベイズ推定を行えば、それぞれの成分について事後分布が得られます。「先月の売上増のうち、実力の向上によるものはどれくらいで、季節要因はどれくらいか」という問いに、点推定ではなく幅つきで答えられるようになります。

状態空間モデルの推定には、大きく二つの道があります。
一つ目はカルマンフィルタです。観測方程式と状態方程式がどちらも線形で、ノイズがすべて正規分布に従う場合、状態の事後分布は正規分布のままになります。正規分布は平均と分散の二つの数字で完全に決まりますから、新しい観測値が一つ来るたびに、平均と分散を更新する簡単な計算式で事後分布を追跡できます。この更新式がカルマンフィルタです。
ここで確認しておきたいのは、カルマンフィルタは本質的にベイズ更新そのものだということです。第2章で扱った「事前分布に尤度を掛けて事後分布を得る」という操作を、時点ごとに繰り返しているにすぎません。前の時点の事後分布を状態方程式で一期先に進めたものが今期の事前分布になり、そこに今期の観測値の尤度を掛けて今期の事後分布を得る。この予測と更新の繰り返しが、線形かつ正規分布という条件のもとで閉じた式に書けるというのがカルマンフィルタの正体です。工学分野で1960年代から使われてきた手法ですが、統計学の言葉に翻訳すれば逐次ベイズ更新です。
二つ目がMCMCです。線形でない場合、あるいはノイズが正規分布でない場合、事後分布は正規分布から外れ、閉じた式では書けなくなります。たとえば観測値が件数データでポアソン分布に従う場合、観測方程式は \( y_t \sim \text{Poisson}(\exp(\mu_t)) \) のような非線形の形になります。あるいは、外れ値の影響を抑えるために観測ノイズを裾の厚いt分布にする場合も同様です。こうした場合は第4章で扱ったMCMCを使い、状態の系列全体を一つの高次元パラメータとみなしてサンプリングします。
この二つは対立するものではなく、使い分けです。線形正規の枠に収まるなら、カルマンフィルタは圧倒的に高速で、データ点が数十万あってもすぐに終わります。そこから外れる要求(件数データ、裾の厚い分布、階層構造、説明変数の効果を時変にする、など)が出てきたときにMCMCに移る、という順序が実務的です。実際、StanやPyMCといったツールの内部では、線形正規の部分についてはカルマンフィルタで周辺尤度を計算し、分散パラメータについてだけMCMCでサンプリングする、という組み合わせが使われることがあります。この組み合わせは計算効率が高く、状態の系列を直接サンプリングするより収束が安定します。
非線形で非正規の場合には、粒子フィルタ(逐次モンテカルロ法)という別の系統の手法もあります。多数の粒子で事後分布を表現し、時点が進むごとに重みを更新して再抽出する方法です。オンラインでデータが逐次到着する状況で、その都度すぐに状態の推定を更新したい場合に向きます。バッチで全期間のデータを一度に扱ってよいならMCMCの方が扱いやすいことが多いです。
なお、ARIMAとの関係を簡単に述べておきます。ARIMAモデルは差分と自己回帰と移動平均の組み合わせで系列を記述する手法で、実は多くのARIMAモデルは状態空間モデルの形に書き直せます。両者は表現力の点で大きく重なりますが、視点が違います。ARIMAは系列の統計的な性質(自己相関の構造)を再現することに主眼があり、状態空間モデルは水準、トレンド、季節といった解釈可能な成分に分けることに主眼があります。予測精度だけを求めるならどちらでもよい場面が多いのですが、「なぜそう予測されるのか」を関係者に説明する必要がある場面では、成分に分解される状態空間モデルの方が伝えやすいです。
状態空間モデルを使うときに混同しやすいのが、三つの異なる問いです。いずれも「時点 \( t \) の状態はどうだったか」を問うのですが、どこまでのデータを使ってよいかが違います。
フィルタリングは、時点 \( t \) までのデータだけを使って時点 \( t \) の状態を推定することです。リアルタイムの監視に対応します。今日までのデータで今日の実力を判断する、という使い方です。
平滑化は、全期間のデータを使って過去の各時点の状態を推定し直すことです。過去の分析に対応します。三か月前の状態を、その後の三か月分のデータも踏まえて振り返る、という使い方です。後から見た方が情報が多いので、平滑化の推定値はフィルタリングの推定値より信用区間が狭くなります。
予測は、時点 \( t \) までのデータを使って時点 \( t+h \) 以降の状態と観測値を推定することです。先に行くほど不確実性が増えるので、信用区間は末広がりに広がります。この末広がりの形そのものが有用な情報で、「三か月先までなら誤差は許容範囲だが、半年先は幅が広すぎて計画の根拠にならない」といった判断ができます。
三つを区別しないまま「モデルの当てはまりがよい」と評価すると、実力を誤って見積もります。平滑化の結果を使って過去のグラフを描けば、当然きれいに合います。将来のデータも使って推定しているのですから当たり前です。予測性能を評価したいなら、ある時点までのデータだけでモデルを推定し、その先を予測して実測と比べる、という手順を時点をずらしながら繰り返す必要があります。
状態空間モデルが実務で重宝される理由の一つが、データの欠けや異常に自然に対応できる点です。
まず欠測です。ある日のデータが取得できなかった場合、多くの手法では補完値を入れるか、その期間を除外するかの判断を迫られます。状態空間モデルでは、その時点の観測方程式を単に使わなければよいだけです。状態方程式は時間を進め続けるので、状態の推定は継続され、観測がなかった期間だけ信用区間が広がります。データがないのだから不確かである、という当たり前のことが結果に反映されます。系統の切り替えでログが数日欠けた、休業日で売上がゼロではなく「存在しない」といったケースを、無理な補完なしに扱えます。
次に外れ値です。観測ノイズを正規分布にすると、大きく外れた一点が状態の推定を強く引っ張ります。これを避けるには、観測ノイズを自由度の小さいt分布に替えます。t分布は正規分布より裾が厚いため、大きく外れた値は「この分布ならたまに起きる」と解釈され、状態の推定はあまり動きません。台風で一日だけ売上が落ちた、システム障害で一日だけアクセスが激減した、といった単発の事象に対して、モデルが過剰反応しなくなります。この変更は分布を差し替えるだけで済み、モデルの骨格は変わりません。ただしt分布を使うと線形正規の条件から外れるため、推定はMCMCで行うことになります。
三つ目が構造変化です。価格改定、主要チャネルの追加、競合の参入、規制の変更。こうした出来事の前後で、系列の性質が変わることがあります。状態空間モデルには二つの対処法があります。一つは、状態の変動分散を裾の厚い分布にして、大きなジャンプを許す形です。ふだんは小さく動き、まれに大きく跳ぶ、という挙動をモデル自体が持つようになり、変化点を事前に指定しなくても検出できます。もう一つは、介入の時点が分かっている場合に、その時点以降に1を取る説明変数を観測方程式に加える形です。介入効果の大きさが係数の事後分布として推定され、「値上げによる水準の低下は何パーセントで、その推定にはどれだけの幅があるか」という形で答えが出ます。
この介入効果の推定は、施策評価の道具として使えます。施策がなかった場合の系列をモデルから予測し、実際の系列と比較する、という考え方です。予測には信用区間が付いているので、実測がその区間から明確に外れているかどうかで効果の有無を判断できます。差分の差分法などの手法と発想は近いのですが、状態空間モデルの場合は反実仮想の予測に不確実性の幅が最初から付いてくる点が違います。因果推論の詳細な議論は本コラムの範囲を超えますが、時系列の構造を押さえたうえで施策の前後を比べるという発想は覚えておく価値があります。
実装の雰囲気を示します。PyMCではランダムウォークを表す分布が用意されているので、状態方程式はそれを使って一行で書けます。以下は水準だけを持つローカルレベルモデルの記述例です。
import numpy as np
import pymc as pm
# y は観測値の1次元配列(欠測は np.nan で表現しておく)
T = len(y)
with pm.Model() as local_level:
# 状態のブレの大きさ(小さいほど state はなめらかになる)
sigma_state = pm.HalfNormal("sigma_state", sigma=1.0)
# 観測ノイズの大きさ
sigma_obs = pm.HalfNormal("sigma_obs", sigma=1.0)
# 状態方程式: mu_t = mu_{t-1} + eta_t
mu = pm.GaussianRandomWalk(
"mu",
sigma=sigma_state,
init_dist=pm.Normal.dist(mu=float(np.nanmean(y)), sigma=10.0),
shape=T,
)
# 観測方程式: y_t = mu_t + eps_t
# observed に nan を含めると、その時点の尤度は評価されない
pm.Normal("y_obs", mu=mu, sigma=sigma_obs, observed=y)
idata = pm.sample(draws=2000, tune=2000, target_accept=0.9)
トレンドを加えるなら、傾きを表す別のランダムウォークを用意し、その累積和を水準に足します。季節成分を加えるなら、周期の長さぶんの和がゼロ付近に収まるよう制約を課した確率変数を用意します。外れ値に強くしたいなら、最後の観測方程式の pm.Normal を pm.StudentT に替え、自由度のパラメータを追加します。いずれの拡張も、モデルの構造を宣言的に書き足すだけで済みます。
推定後は、第4章で述べた収束診断を必ず確認します。状態空間モデルは状態の数が時点数と同じだけあるため、パラメータ空間の次元が高く、サンプリングが難しくなりがちです。特に \( \sigma_{\text{state}} \) と \( \sigma_{\text{obs}} \) は互いに強く相関し、どちらが大きいかの識別が弱いことがあります。事前分布を弱情報的に設定して極端な値を抑える、非中心化と呼ばれる書き換えを使う、あるいは前述のようにカルマンフィルタで状態を積分消去してから分散だけをサンプリングする、といった対処が有効です。
ここまでの内容を、意思決定の側から整理します。
第一に、日々の変動に反応しないための道具として使えます。ダッシュボードの折れ線が下がったときに、それが状態の変化なのか観測ノイズなのかを、モデルは分けて答えます。状態の信用区間が先週と重なっているなら、その下落は雑音の範囲内であり、対策会議を開く根拠にはなりません。これは何もしないことを正当化する仕組みではなく、限られた注意をどこに向けるかを決める仕組みです。
第二に、変化の検知に使えます。状態の推定値が、過去の水準から見て統計的に意味のある形で動いたかどうかを継続的に監視できます。閾値を固定値で決める従来の監視は、季節変動やトレンドがあると誤報が増えますが、状態空間モデルは季節とトレンドを分離したうえで残りの動きを見るため、意味のある異常だけを拾いやすくなります。
第三に、計画の前提を確率で語れるようになります。予測が点ではなく分布として出るので、「来期の売上が計画値を下回る確率は何パーセントか」という問いに直接答えられます。第3章で扱った信用区間の考え方が、そのまま将来の値に適用できるわけです。在庫の発注量や人員配置のように、上振れと下振れで損失の構造が非対称な判断では、この分布があるかないかで結論が変わります。品切れの損失が過剰在庫の損失の三倍なら、分布の中央値ではなく上側の分位点を基準に発注すべきだ、という判断が定量的にできます。
第四に、成分に分けて説明できます。「今期の増加は季節要因が大半で、実力の水準はほぼ横ばい」という説明は、単に予測値を出すより関係者の納得を得やすく、次の打ち手の議論につながります。ここが、精度は高いが中身の見えないモデルとの実務上の差になります。
一方で、万能ではありません。状態空間モデルが前提としているのは、過去の構造が将来もある程度続くことです。全く新しい商品の立ち上げ期、前例のない市場環境の激変、データ自体の定義が変わった直後。こうした場面では、過去から学べることが少なく、モデルの信用区間は実態より狭く出る危険があります。モデルが示す不確実性は、あくまで「このモデルが正しいとしたときの不確実性」であり、モデルそのものが間違っている可能性は含まれていません。この区別は、結果を経営に報告する際に必ず添えるべき注意点です。どのモデルをいつ使い、いつ使わないかについては第12章で改めて整理します。
『時系列分析と状態空間モデルの基礎:RとStanで学ぶ理論と実装』(馬場真哉、プレアデス出版):時系列の基礎から状態空間モデルまでを、数式と実装の両面から段階的に積み上げる構成で、この章の内容を手を動かして確かめたい場合の入口として使えます。
『予測にいかす統計モデリングの基本:ベイズ統計入門から応用まで』(樋口知之、講談社):状態空間モデルを予測という目的から一貫して説明し、カルマンフィルタから粒子フィルタまでの流れを見通しよく扱っています。
ここまでの章では、パラメータを1つの値として決め打ちせず分布として扱うこと、そしてその分布から「どれくらい確からしいか」を読み取ることを、繰り返し扱ってきました。この章では、その考え方を深層学習に持ち込みます。画像分類や需要予測に使われるニューラルネットワークは高い精度を出しますが、その予測が「どれくらい信じてよいものか」を教えてくれません。予測が当たっているときも外れているときも、同じ顔をして答えを返してきます。業務にAIを組み込むとき、この性質は思っている以上に厄介です。
本章で扱うのは、深層学習の予測に不確実性の目盛りを付ける方法です。理論の枠組みとしてのベイズニューラルネットワークを整理し、実務で最も手軽に使える近似としてモンテカルロ・ドロップアウトを取り上げます。手書き数字データセットMNISTを使った実験を通して、「モデルが自信を持てない入力」が実際にどんなものなのかを目で確かめます。最後に、こうして得た不確実性を業務プロセスにどう組み込むか、つまり誤りコストの高い業務で人間の確認をどこに集中させるかという設計の話に接続します。
分類を行うニューラルネットワークの最終層には、ふつうソフトマックス関数が置かれます。出力はクラスごとの非負の値で、合計すると1になります。「クラス3である確率が0.98」という形で読めるので、これをそのまま確信度として使っている現場は多いはずです。数値としては確率の形式を満たしていますし、閾値を切って「0.9未満なら保留」といった運用も、一見すると自然に見えます。
ところが、この値は本来の意味での不確実性ではありません。理由は二つあります。
一つ目は、ソフトマックスの出力が「学習済みの重みが正しいと仮定したうえでの、クラスの相対的な確からしさ」でしかないことです。学習によって決まった重みは、有限のデータから推定された1組の値にすぎません。データが違えば別の重みになりますし、初期値や学習の順序が違うだけでも変わります。つまり重み自体に推定の不確かさがあるのに、ソフトマックスの計算はその不確かさをまったく通していません。前章までの言葉で言えば、事後分布の平均だけを使って予測し、分布の広がりを捨てているのと同じ状態です。ベイズ統計が繰り返し警告してきた「点推定だけを見ると危ない」という話が、そのまま当てはまります。
二つ目は、学習時に使われた損失関数の性質です。分類の学習では交差エントロピーを最小化しますが、これは正解クラスの出力をできるだけ1に近づける方向に働きます。層が深く表現力の高いモデルほど、訓練データに対して極端に自信のある出力を作れてしまい、その癖が未知データにも持ち越されます。近年の研究では、現代的な深層学習モデルは総じて出力確率が実際の正答率より高く出る傾向、いわゆる較正のずれを持つことが指摘されています。「0.99」と言っているのに実際には100回に5回外す、といったことが普通に起こります。
もっと悪いのは、学習時に見たことのない種類の入力が来たときです。手書き数字だけを学習したモデルにアルファベットの画像を入れても、ソフトマックスは10クラスのどれかに強い値を割り当てて答えます。「知らないものを見た」という状態を表現する出口が、そもそもモデルの構造の中に用意されていないためです。検品ラインに新しい不良モードが現れたとき、与信モデルが経験のない属性の申込を受け取ったとき、モデルは黙って自信満々に間違えます。
必要なのは、クラス間の相対的な確からしさではなく、「このモデルは、この入力に対して答えを出せる立場にあるのか」という別種の情報です。これを取り出す枠組みが、ベイズの側から用意されています。
発想自体は、これまでの章とまったく同じです。ニューラルネットワークの重み \( {\boldsymbol \omega} \) を1つの値に決めるのではなく、確率変数として扱います。学習データ \( \textbf{X}, \textbf{Y} \) を観測したあとの重みの事後分布 \( p({\boldsymbol \omega}|\textbf{X}, \textbf{Y}) \) を考え、予測のときはこの分布全体にわたって平均をとります。これをベイズニューラルネットワークと呼びます。
新しい入力 \( \textbf{x} \) に対する予測分布は、次のように書けます。
\( p(\textbf{y}|\textbf{x}, \textbf{X}, \textbf{Y}) = \displaystyle\int p(\textbf{y}|\textbf{x}, {\boldsymbol \omega}) \, p({\boldsymbol \omega}|\textbf{X}, \textbf{Y}) \, d{\boldsymbol \omega} \)
右辺は、「ありえる重みの組それぞれで予測を出し、その重みがどれくらいもっともらしいかで重みづけして混ぜる」という操作です。データから重みが強く定まっている領域では、どの重みを使っても同じ答えになるので予測分布は鋭く尖ります。逆に、データが乏しい領域では重みが変わると答えも変わるため、予測分布は広がります。この広がりこそが、求めていた不確実性です。第3章で扱った信用区間や、第7章の階層モデルにおける情報の借り合いと、同じ論理の上に立っています。
問題は計算です。この積分は、重みの次元がせいぜい数個であれば数値的に扱えますが、実際のニューラルネットワークの重みは数十万から数億個あります。第4章で見たMCMCも、この規模では現実的な時間で事後分布を巡回できません。事後分布そのものが多峰的で、局所解が無数にあることも効きます。そのため、ベイズニューラルネットワークは長らく「理屈は美しいが実務では重い」という位置づけにありました。
そこで用いられるのが変分推論の考え方です。真の事後分布 \( p({\boldsymbol \omega}|\textbf{X}, \textbf{Y}) \) を直接求める代わりに、扱いやすい形の近似分布 \( q({\boldsymbol \omega}) \) を置き、両者が近くなるように \( q \) のパラメータを調整します。積分を最適化問題に置き換える手法だと考えると分かりやすいと思います。近似分布の形をどう選ぶかで手法の性格が決まり、実装のしやすさと精度のバランスもそこで決まります。
ここで話が実務に一気に近づきます。ある特定の近似分布の選び方をすると、その学習手続きが、深層学習の世界で以前から広く使われてきたある技法とぴったり一致することが示されました。それがDropoutです。
根拠となる論文は次のものです。
この論文は、Dropoutを適用して学習した深層学習が、ディープなガウス過程における近似ベイズ推論として解釈・定式化できることを理論的に示しています。ガウス過程は、個々のパラメータではなく関数そのものに確率分布を考える枠組みで、本章で扱うMC Dropoutによる近似の理論的な裏付けにあたる位置づけです。
Dropoutはもともと、過学習を防ぐための工夫として提案されたものです。学習のたびに、ネットワークのユニットを一定の確率でランダムに0にします。特定のユニットに依存しすぎる状態を壊すことで、汎化性能を保つ狙いがあります。実装は数行で済み、画像認識でも自然言語処理でも定番の部品になっています。
少し整理します。学習データ \( \textbf{X}, \textbf{Y} \) が与えられたとき、ニューラルネットワークの重み \( {\boldsymbol \omega} \) の事後分布 \( p({\boldsymbol \omega}|\textbf{X}, \textbf{Y}) \) を直接求めるのは困難です。そこで、これを近似する分布 \( q({\boldsymbol \omega}) \) を考えます。論文は、この近似分布 \( q({\boldsymbol \omega}) \) からの重みのサンプリングが、Dropoutによってネットワークのユニットをランダムに0にすることと同じ意味になることを示しました。つまり、Dropoutをかけて1回推論するという行為が、重みの事後分布から1組サンプリングして予測するという行為に対応します。
そのうえで、出力 \( \textbf{y} \) の予測分布は、Dropoutを適用したまま推論を複数回繰り返し、その平均をとることで近似できます。論文ではこれをMonte Carlo dropout(モンテカルロ・ドロップアウト、以下MC Dropout)と呼んでいます。
\( p(\textbf{y}|\textbf{x}, \textbf{X}, \textbf{Y}) \approx \displaystyle\frac{1}{T}\sum_{t=1}^{T} p(\textbf{y}|\textbf{x}, {\boldsymbol \omega}_t) \)
ここで \( T \) はサンプリング回数、\( {\boldsymbol \omega}_t \) は \( t \) 回目のDropoutで得られた重みです。通常、Dropoutは推論時には無効にしますが、MC Dropoutでは推論時もDropoutを有効にしたまま \( T \) 回推論する、というのがポイントです。
実務的な価値は、この一行に集約されます。すでにDropoutを入れて学習したモデルがあるなら、モデルを作り直す必要も、追加で学習し直す必要もありません。推論のコードで推論時のDropoutを有効にし、同じ入力を \( T \) 回通して結果を集めるだけです。第4章で見たMCMCのサンプリングを、ネットワークの前向き計算で代用していると考えると、位置づけがつかみやすいと思います。
予測分布の不確実性(予測しにくさ)を表す指標としては、論文では分散やエントロピーの利用が提案されています。本章では、このうちエントロピーを使って、各画像の予測しにくさを定量化してみます。エントロピーは確率分布の予測しにくさを表す指標で、確率分布が一様分布に近いほど(どのラベルとも判断がつかないほど)大きくなります。
クラスごとの平均予測確率を \( p_k \) と書くと、予測エントロピーは次の式で計算します。
\( H = -\displaystyle\sum_{k} p_k \log p_k \)
10クラスの分類で、ある1クラスに確率が集中していればエントロピーは0に近づき、10クラスに均等に散らばると最大値の \( \log 10 \)(約2.30)に近づきます。数値が大きいほど「どのラベルとも判断がつかない」という状態を表します。

それでは実際に、Dropoutを適用して深層学習モデルを学習し、Dropoutを適用したまま推論を繰り返して予測分布を作成してみます。論文と同様に、MNIST画像分類タスクで実験してみます。MNISTは28ピクセル四方の手書き数字画像を0から9の10クラスに分類する、機械学習の標準的なベンチマークです。
まずは必要なライブラリの読み込みとMNISTデータの準備です。
import numpy as np
import matplotlib.pyplot as plt
import torch
import torch.nn as nn
import torch.nn.functional as F
from torch.utils.data import DataLoader
from torchvision import datasets, transforms
from tqdm import tqdm
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
transform = transforms.ToTensor()
train_dataset = datasets.MNIST(root='./data', train=True, download=True, transform=transform)
valid_dataset = datasets.MNIST(root='./data', train=False, download=True, transform=transform)
train_loader = DataLoader(train_dataset, batch_size=1000, shuffle=True)
valid_loader = DataLoader(valid_dataset, batch_size=1000, shuffle=False)
len(train_dataset), len(valid_dataset) # (60000, 10000)
学習用が6万枚、検証用が1万枚です。
モデルのアーキテクチャは、畳み込みを少し加えた簡単なCNNにします。全結合層のあとにDropoutを入れている点がポイントです。ここでDropoutを入れておかないと、あとからMC Dropoutを適用できません。逆に言えば、既存のモデルにDropout層が入っていれば、そのまま流用できます。
class Model(nn.Module):
def __init__(self):
super().__init__()
self.conv1 = nn.Conv2d(1, 16, 3)
self.conv2 = nn.Conv2d(16, 32, 3)
self.fc3 = nn.Linear(32 * 5 * 5, 1000)
self.fc4 = nn.Linear(1000, 1000)
self.fc5 = nn.Linear(1000, 10)
self.dropout = nn.Dropout(p=0.5)
def forward(self, x):
h1 = F.max_pool2d(F.relu(self.conv1(x)), 2)
h2 = F.max_pool2d(F.relu(self.conv2(h1)), 2)
h2 = h2.flatten(1)
h3 = self.dropout(F.relu(self.fc3(h2)))
h4 = self.dropout(F.relu(self.fc4(h3)))
y = self.fc5(h4)
return y
Dropoutの確率は0.5に設定しています。この値は近似分布の広がりを決めるパラメータでもあるため、不確実性の大きさそのものに影響します。実務では、確率を変えて不確実性の分布がどう動くかを確認しておくとよいと思います。
モデルを学習させます。学習の手続き自体は、ベイズを意識しない通常の分類モデルとまったく同じです。
model = Model().to(device)
optimizer = torch.optim.Adam(model.parameters(), lr=1e-4)
criterion = nn.CrossEntropyLoss()
epoch_num = 10
for epoch in range(epoch_num):
model.train()
for x, y in train_loader:
x, y = x.to(device), y.to(device)
optimizer.zero_grad()
loss = criterion(model(x), y)
loss.backward()
optimizer.step()
# 検証
model.eval()
correct = 0
with torch.no_grad():
for x, y in valid_loader:
x, y = x.to(device), y.to(device)
correct += (model(x).argmax(1) == y).sum().item()
print(f'epoch {epoch + 1}: valid accuracy = {correct / len(valid_dataset):.4f}')
10エポックほど学習させると、検証データで98%程度の精度になります(環境や乱数により多少前後します)。
ここまでで得られたのは、ごく普通の分類モデルです。ベイズの要素はまだ何も入っていません。特別な学習手法も、特別な損失関数も使っていません。この点が、MC Dropoutが実務で採用されやすい最大の理由です。学習パイプラインを変えずに済むということは、既存の資産をそのまま使えるということでもあります。
続いて、本題のMC Dropoutによる予測分布を作成します。通常、PyTorchではmodel.eval()でDropoutを無効にして推論しますが、ここでは意図的にDropoutを有効な状態(model.train())にして推論を繰り返します(今回のモデルには含まれませんが、BatchNormなどtrain/evalで挙動が変わる層がある場合は、それらはeval()のままDropout層だけを有効化する必要があります)。1回の推論で各クラスの予測確率ベクトル(softmax出力)が1つ得られ、これをサンプリング回数だけ繰り返して平均することで、「結局どのラベルにどれだけ振り分けられたのか」という予測分布を近似できます。
1枚の画像について推論を繰り返したとき、すべてのラベルに均等に振り分けられれば(一様分布に近ければ)、その画像はどのラベルか判断がつかない、つまり予測しにくい画像ということになります。逆に、予測しやすい画像は、どんなDropoutパターンでも予測ラベルが特定の値に集中します。この予測しにくさを、エントロピーで定量化します。
以下は、バリデーションデータの中からtarget_numのラベルに絞ってエントロピーを算出し、予測しやすい画像と予測しにくい画像をプロットするコードです。
def plot_entropy_examples(target_num, sampling_num=50):
# 対象ラベルの画像だけ抽出
valid_x = valid_dataset.data.float().unsqueeze(1) / 255.0 # (N, 1, 28, 28)
valid_y = valid_dataset.targets
target_x = valid_x[valid_y == target_num].to(device)
# MC Dropout:推論時もDropoutを有効にする
model.train()
entropy = np.zeros(len(target_x), dtype=np.float32)
with torch.no_grad():
for i in tqdm(range(len(target_x))):
x = target_x[i].unsqueeze(0)
preds = np.zeros((sampling_num, 10), dtype=np.float32)
for j in range(sampling_num):
preds[j] = F.softmax(model(x), dim=1).cpu().numpy().squeeze()
preds = preds.mean(axis=0)
entropy[i] = np.sum(-preds * np.log(preds + 1e-12))
target_imgs = (target_x.cpu().numpy().reshape(-1, 28, 28) * 255).astype(np.uint8)
low_entropy_imgs = target_imgs[np.argsort(entropy)[:30]]
high_entropy_imgs = target_imgs[np.argsort(entropy)[::-1][:30]]
for title, imgs in [('low entropy top 30', low_entropy_imgs),
('high entropy top 30', high_entropy_imgs)]:
fig, axs = plt.subplots(ncols=10, nrows=3, figsize=(20, 5))
for i, img in enumerate(imgs):
axs[i // 10, i % 10].imshow(img, cmap='gray')
axs[i // 10, i % 10].axis('off')
plt.suptitle(title)
plt.show()
plot_entropy_examples(target_num=0) # 試しに0の画像で検証
サンプリング回数は50回に設定しています。np.log(preds + 1e-12)の微小項は、確率が0になったときに対数が発散するのを避けるための処置です。

予想通りの結果になりました。バリデーションの中から0の画像で、MC Dropoutで得られた予測分布のエントロピーが低かったTOP30、高かったTOP30を表示しています。エントロピーが低いものは予測がしやすい画像なので、とても綺麗にお手本のように書かれた0が集まりました。逆にエントロピーが高いものは予測がしにくく、他のラベルと間違えやすい画像なので、形がいびつだったり汚い字が集まっています。
これを他の数字でも実行すると、同じ傾向が繰り返し現れます。
1の画像では、エントロピーが低いものはただ真っ直ぐに線が引かれているだけで、間違いようがなさそうです。エントロピーが高いものは、字がかすれていたり、線が太すぎたりして予測を間違えやすい傾向にあるようです。
2の画像では、エントロピーが高いものはかなりひどく、人でも読めなさそうなものも見受けられます。
3の画像では、エントロピーが低いものはとても綺麗にバランスの取れたお手本のような3が集まりました。エントロピーが高いものは、読めないことはなさそうですが、やはりバランスが悪い字が多いです。
4以降も同様の傾向で、エントロピーが低いものはバランスが良く綺麗な字、高いものは崩れた字が集まります。
この結果の意味するところを、少し丁寧に受け止めたいと思います。ここで使ったのは、正解ラベルの情報ではありません。エントロピーの計算には、その画像が実際に何の数字だったかという情報は一切入っていません。モデルの内部状態だけから「この入力は自信を持って答えられない」と自己申告させ、その順にソートしただけです。それにもかかわらず、人間の目で見ても確かに読みにくい字が上位に集まっています。モデルの自己申告は、それなりに信用できるということです。
そして、これは正解ラベルがない本番環境でも同じように計算できます。ここが実務上の決定的な違いです。精度や誤り率は正解が判明してからでないと計算できませんが、予測エントロピーは予測と同時に、その場で出せます。
このように、Dropoutを入れるだけで様々な深層学習のネットワークアーキテクチャに適用でき、結果も見ていて面白いです。欠点があるとすれば、1つの入力につきサンプリング回数だけ推論を繰り返すため、予測に少し時間を要することです。50回サンプリングすれば、単純計算で推論コストは50倍になります。バッチ処理であれば許容できることが多いものの、応答時間に制約のあるリアルタイム推論では、サンプリング回数を減らすか、後述する別の手法を検討することになります。また、予測がしにくいデータは教えてくれますが、「なぜ予測しにくいのか」「どうすれば間違えにくくなるのか」は、結果を見て自分で考察していく必要があります。
このような方法の応用例として、物体検出のモデルに適用し、予測確率が高そうなバウンディングボックスを重ねて可視化する、以下のような論文も出ています。
ここでは、実装上の細かい疑問に対する検証を行います。
論文は予測の平均・分散・エントロピーといった指標には触れていますが、ここで試すような実装上の計算順序の違いまでは主題にしていません。このとき、エントロピーを使うにしても、計算の順序として例えば次の2通りが考えられます。
どちらも問題なさそうな気がしますが、どちらがより妥当なのかが疑問に思われるところです。そこで、学習・予測データやDropoutなどの乱数を固定したうえで、両方の結果を見比べてみます。
1つ目は前節と同じ、出力ベクトル → Softmax → 平均 → エントロピーのパターンです(前節のplot_entropy_examplesがこれにあたります)。MC Dropoutサンプリングごとにsoftmaxをとり、その平均からエントロピーを計算しています。
2つ目は、出力ベクトル → 平均 → Softmax → エントロピーのパターンです。MC Dropoutサンプリングの出力ベクトル(softmax前)の平均をとってから、softmaxをかけてエントロピーを算出します。コードは、サンプリング部分を以下のように変更します。
# パターン2:出力ベクトルの平均をとってからSoftmax
for j in range(sampling_num):
preds[j] = model(x).cpu().numpy().squeeze() # softmax前の出力
mean_logits = preds.mean(axis=0)
prob = F.softmax(torch.from_numpy(mean_logits), dim=0).numpy()
entropy[i] = np.sum(-prob * np.log(prob + 1e-12))
結果は、やはり完全一致はしませんが、傾向としては同じようなものになりました。
さらに、Dropoutなし(通常の推論)で 出力ベクトル → Softmax → エントロピー を計算してみると、結果は以下のようになります。
# Dropoutなし(model.eval())で1回だけ推論
model.eval()
with torch.no_grad():
prob = F.softmax(model(x), dim=1).cpu().numpy().squeeze()
entropy[i] = np.sum(-prob * np.log(prob + 1e-12))
こちらも傾向は同じになりました。ということは、例えば目的が能動学習(予測しにくいデータを優先的にラベル付けする手法)に用いるなどであれば、いずれの方法でも似たような効力が得られそうな気がします。
とはいえ、MC Dropoutサンプリングを導出することでベイズの枠組みとして考えられることは論文で理論的に定式化されていますので、サンプリングから予測分布を導出する形まで、数式的にはMC Dropoutが最も納得のいく方法だと思います。
この検証から実務向けに引き出せる教訓は二つあります。一つは、順序付け(どのデータが怪しいか)が目的であれば、実装の細部にそれほど神経質にならなくてよいこと。もう一つは、出てきたエントロピーの値そのものを閾値として使い、「0.5を超えたら人間に回す」といった絶対的な基準にする場合には、計算方法が変われば値のスケールも変わるため、必ず自分たちのデータで閾値を引き直す必要があることです。値の水準を他所から借りてくることはできません。
不確実性推定(Uncertainty Quantification)は、その後も活発に研究されています。2026年時点での実務での位置づけを整理します。
手軽に試せるMC Dropoutから始めて、要求される精度やコストに応じてアンサンブルやEvidential系へ広げていく、という使い分けが現在の実務的な流れです。
2分類については、経営判断の観点からも押さえておく価値があります。偶然的不確実性は、データを増やしても減りません。手書き文字の例で言えば、書いた本人にしか判別できないほど崩れた字は、学習データを10倍にしても読めるようになりません。これは撮影条件やセンサーの精度、業務プロセスそのものに由来するばらつきなので、減らしたければデータではなく現場の側を変える必要があります。撮影機材を変える、記入フォーマットを変える、といった投資です。
一方、認識的不確実性はデータを増やせば減ります。モデルが経験していない領域だから自信が持てないのであって、その領域のデータを集めて学習すれば解消します。追加のデータ収集やアノテーションへの投資が効くのはこちら側です。
不確実性が高い予測が続いたとき、「データをもっと集めよう」で正しいのか、「そもそも入力データの質を上げないと無理」なのか。この判断を分けるのが2分類の実務的な意義です。分類が曖昧なまま追加アノテーションの予算を投じても、偶然的不確実性が支配的な領域では成果が出ません。
ここまでの内容を、投資判断につながる形に翻訳します。
不確実性を測ることの実務価値は、精度が上がることではありません。MC Dropoutを入れてもモデルの正答率はほとんど変わりません。価値は、モデルの出力を「全部信じる」か「全部疑う」かの二択から解放し、案件ごとに人間の手を入れる場所を選べるようにすることにあります。予測を、確信度の順に並べ替えられるようになるということです。
運用の形はシンプルです。予測と同時に不確実性を計算し、閾値を超えたものだけを人間の確認に回します。閾値を下回ったものは自動処理でそのまま流します。これがhuman-in-the-loop、つまり人間を工程の中に残す設計です。
この設計が効くのは、誤りのコストが一様でない業務です。3つの典型を挙げます。
医療画像の読影支援では、見落としのコストが極端に高い一方、専門医の時間は限られています。全件を医師が見るなら自動化の意味がなく、全件を自動判定に任せるのはリスクが取れません。不確実性の高い症例だけを医師に回せば、限られた時間を最も判断が難しい症例に集中させられます。
製造ラインの外観検品では、不良を見逃せば市場での回収につながり、良品を不良と判定すれば歩留まりが落ちます。過去に見たことのない不良モードが現れたとき、通常のモデルは既知のどれかに強引に分類しますが、不確実性を見ていれば「知らないものが来た」という信号として拾えます。新規不良モードの早期発見という、精度指標には現れない効果があります。
与信審査では、誤った承認は貸倒れに、誤った否決は機会損失と顧客の不満につながります。加えて、判断の根拠を説明する責任もあります。不確実性の高い申込を人間の審査に回す運用は、リスク管理と説明責任の両方に効きます。
投資対効果は、比較的単純な形で見積もれます。全件を人間が確認する体制のコストを基準に、不確実性の低い予測を自動処理に回したときに削減できる工数を見ます。そこから、自動処理に回した分で誤りが発生する確率と、その誤り1件あたりのコストを差し引きます。ここで重要なのは、閾値を動かすと自動化率と誤り率が同時に動くことです。検証データで閾値をいくつか試し、自動化率と、自動処理に回した分の精度を並べた表を作れば、業務側と「どこまで自動化するか」を数字で議論できます。
導入時に押さえるべき点をいくつか挙げます。
やり方自体はシンプルですので、Dropoutを含むモデルを既に運用しているのであれば、推論のコードを少し変えるだけで試せます。まず自社のデータでエントロピーを計算し、値が高かった上位の事例を実際に目で確認するところから始めるのが、最も納得を得やすい進め方だと思います。人間が見ても迷う事例が上位に並べば、その時点で仕組みの妥当性は現場に伝わります。
次章では、ここまで扱ってきたベイズの手法群を俯瞰し、実務でいつ使うべきか、そしていつ使わないほうがよいのかを整理します。
『ベイズ深層学習』(須山敦志、講談社):本章で扱ったベイズニューラルネットワークと変分推論を、数式の導出から丁寧に追える一冊です。MC Dropoutが近似ベイズ推論として位置づけられる理由を、自分の手で確かめたい方に向いています。
『ガウス過程と機械学習』(持橋大地・大羽成征、講談社):Dropoutによる近似の背景にあるガウス過程を、基礎から解説した和書です。「予測に幅がつく」という考え方の源流を押さえられるので、本章の理論的な足場を固めたいときに役立ちます。
ここまでの章で、ベイズの考え方(第1章)、事前分布と事後分布の仕組み(第2章)、信用区間による表現(第3章)、MCMCという計算手段(第4章)、StanとPyMCによる実装(第5章)、検定の代替としての使い方(第6章)、階層モデル(第7章)、対戦データと感染症の実例(第8章・第9章)、状態空間モデル(第10章)、ベイズ深層学習(第11章)を見てきました。手法の引き出しは一通りそろったことになります。
最後に残るのは、どの案件でこの引き出しを開けるかという判断です。ベイズは万能ではなく、向く問題と向かない問題があります。向かない問題に持ち込むと、計算時間だけがかさみ、報告書は読みにくくなり、社内の理解も得られません。本章では、採否をどう判断し、どう小さく導入し、どう運用し、どう報告するかを整理します。技術の章ではなく、体制と業務プロセスの章です。
ベイズを使うべきかどうかを、モデルの精度だけで決めることはできません。同じデータに対して、頻度論的な回帰でも勾配ブースティングでも階層ベイズでも、当てはまりの良さはそれほど変わらないことがあります。それでも採否が分かれるのは、その分析結果が最終的にどんな意思決定につながるかが案件ごとに違うからです。
分析の出口には大きく2種類あります。1つは、予測値そのものが自動的に次の処理に流れていく出口です。レコメンドの並び替え、需要予測に基づく発注量の自動計算、異常検知のアラートなどが該当します。この場合、人が結果を読む機会は少なく、点予測の精度と応答速度が価値のほとんどを占めます。もう1つは、人が結果を読んで判断する出口です。設備投資をするか、価格を変えるか、新製品を出すか、撤退するかといった判断で、これらは一度決めると引き返しにくく、外したときの損失が大きい種類の決定です。
ベイズが本当に効くのは後者です。後者では「見込みは月120万個」という一点の数字よりも、「月90万個から160万個の範囲に95%の確からしさで収まり、100万個を下回る確率は18%」という幅のある情報のほうが、判断の材料として役に立ちます。下回る確率が18%だと分かれば、その18%が起きたときの損失と、備えるための費用を比べられるからです。点予測しか手元にない場合、この比較はそもそも始められません。
したがって採否の第一の問いは「この分析結果を誰が読み、読んだ結果として何を決めるのか」です。読み手が人であり、決定が非対称な損失を伴い、取り返しがつきにくいほど、ベイズの投資対効果は上がります。逆に、結果が機械に流れ込むだけで人が読まないのであれば、ベイズの長所である不確実性の表現は使われないまま計算コストだけが残ります。

実務でベイズを選ぶ理由になりうる条件は、おおむね次の5つに整理できます。1つでも強く当てはまれば検討する価値があり、2つ以上重なるなら有力な選択肢になります。
1つ目のデータが少ない場合についてです。サンプルが数十件しかない、新商品で過去実績がない、対象顧客が限られているといった状況では、標本だけから推定した数値は大きく振れます。10件のうち3件が成約したから成約率30%だと報告すると、実際には10%から60%程度まで幅があるという事実が消えてしまいます。ベイズでは、第2章で扱った事前分布によって「業界の一般的な成約率はこのくらい」という手持ちの知識を明示的に入れ、少ないデータと組み合わせて妥当な範囲に落ち着かせられます。事前分布は恣意的な操作ではなく、前提を数式として文書に残す仕組みだと考えると位置づけがはっきりします。
2つ目のグループ構造についてです。店舗別、地域別、担当者別、機械の号機別、患者の施設別といった単位で分析したい要求は実務に多く現れます。ところが単位を細かくするほど、1単位あたりのデータは減ります。すべてをまとめて1本のモデルにすると個々の違いが消え、単位ごとに完全に別々のモデルを作ると小さな単位の推定が暴れます。第7章の階層ベイズは、この中間を自動的に取ります。データの少ない単位ほど全体平均に寄せ、データの多い単位は自分の実績を強く反映するという調整が、モデルの構造から自然に出てきます。店舗が数百あり、そのうち大半が出店から日が浅いといった状況は、階層ベイズが最も効く典型です。
3つ目の不確実性と意思決定の接続についてです。判断の損失が左右非対称なとき、幅の情報は決定的な意味を持ちます。在庫を切らす損失と余らせる損失が違う、設備を止める損失と壊れるまで使う損失が違う、といった状況です。このとき最適な判断は、予測の中央値ではなく、損失の期待値を最小にする点で決まります。この計算には事後分布そのものが必要で、点予測しか持っていないと原理的に実行できません。詳しくは後述します。
4つ目の事前知識と構造の埋め込みについてです。ベイズモデルは、汎用の機械学習モデルと違って、対象の仕組みを式として書き下せます。第9章で扱った感染症のSIRモデルのように、既知の微分方程式を持つ現象、在庫や待ち行列のように物理的な制約がある現象、購買の意思決定のように理論的な構造が想定できる現象では、その構造をそのままモデルに入れられます。構造を入れると、データが少なくても筋の通った推定ができ、パラメータの意味が経営の言葉と対応します。感染率、離脱率、値段への感度といったパラメータは、そのまま議論の対象になります。
5つ目の逐次更新についてです。第2章で見たとおり、ベイズ更新は今日の事後分布を明日の事前分布として使う形で自然につながります。毎週データが少しずつ増えるモニタリング業務、パイロット導入を段階的に広げていく施策、第10章の状態空間モデルのように時点ごとに状態を更新していく仕組みでは、この性質がそのまま運用設計になります。第6章で扱ったA/Bテストの逐次的な確認も同じ理屈で、途中経過を見ながら意思決定を進めても解釈が破綻しにくいという利点があります。
一方で、ベイズを使わなくてよい、あるいは使わないほうがよい状況も同じくらい明確に存在します。ここを言えることが、実務でベイズを扱う人の信頼につながります。
第一に、データが大量にあり、求められているのが点予測の精度だけである場合です。数十万件から数百万件の学習データがある推薦や広告配信では、事前分布の影響はほとんど無視できる大きさになり、ベイズ推定と最尤推定の結果はほぼ一致します。この状況で得られる追加情報は限られており、勾配ブースティングやニューラルネットワークを素直に使い、精度の改善に労力を割いたほうが成果につながります。ベイズを使うにしても、モデル全体をベイズ化するのではなく、第11章のように予測の自信度が必要な部分だけに近似的な手法を当てるほうが現実的です。
第二に、リアルタイム推論の制約が厳しい場合です。ミリ秒単位で応答しなければならないAPI、端末側で動かす必要がある推論、1日に何億回も呼ばれる処理では、MCMCによる事後サンプリングをその場で回すことは現実的ではありません。回避策として、学習は事前にオフラインで行い、事後分布から得たサンプルを圧縮して保持しておく、あるいは近似推論で軽量化するといった設計はありえますが、それでも運用の複雑さは増します。応答速度が最優先の要件であるなら、無理をしない判断が妥当です。
第三に、組織の中に結果を解釈して使える人がいない場合です。信用区間を示しても「結局いくつなんですか」と点の数字だけが抜き出され、幅の情報が捨てられて意思決定に至るのであれば、ベイズを導入したコストは回収できません。この場合に必要なのは高度なモデルではなく、読み方の共有です。順序としては、まず簡単なモデルで幅のある報告に慣れてもらい、そのうえで手法を高度化するほうが定着します。
第四に、モデルの妥当性を検証する手立てがない場合です。ベイズモデルは事前分布と尤度の指定によって、いくらでも複雑にできます。複雑にすればデータへの当てはまりは良くなりますが、外部のデータで検証できなければ、それが現実を捉えているのか作り込みすぎなのかを区別できません。検証用データを分けられる見込みがなく、専門家によるレビューも受けられないのであれば、解釈のしやすい単純なモデルにとどめる判断が安全です。
第五に、締め切りまでの時間が極端に短い場合です。ベイズモデルの構築は、モデルの定式化、事前分布の設定、収束の確認、モデルの比較といった工程を踏みます。第4章で扱ったとおり、MCMCは収束を確認して初めて結果として使えるもので、この確認を飛ばした結果は数字として意味を持ちません。翌日までに何らかの数字が必要という状況で急いで組み立てると、収束していないサンプルから作った区間を報告するという最悪の事態を招きます。時間がないときは、既存の集計や回帰で答えを出し、ベイズは次の周期に回すほうが確実です。
ここまでの内容を、案件の立ち上げ時に確認する形にまとめると次のようになります。左の観点ごとに、自分の案件が中央と右のどちらに近いかを見ていきます。中央に寄る項目が多いほどベイズの利点が生き、右に寄る項目が多いほど従来手法で十分だと判断できます。
| 観点 | ベイズを選ぶ理由になる状況 | 従来手法で十分な状況 |
|---|---|---|
| データ量 | 数十件から数百件 新規事業で実績が乏しい |
数万件以上 安定して蓄積されている |
| グループ構造 | 店舗・地域・個人など単位が多く 単位あたりの件数が少ない |
単位がない、または 各単位に十分な件数がある |
| 意思決定への接続 | 人が読んで判断する 外したときの損失が非対称 |
機械が自動処理する 誤りの影響が対称で小さい |
| 事前知識 | 過去実績・専門家の知見・ 物理や理論の構造がある |
入れられる知識が特にない データに語らせたい |
| 更新の仕方 | データが少しずつ増える 途中経過を見ながら進める |
一括で学習し 定期的に作り直せばよい |
| 応答速度 | 日次・週次のバッチでよい | ミリ秒単位の応答が必要 |
| 説明の必要性 | 根拠を説明する責任がある 規制対応・社外報告がある |
精度が出れば説明は不要 |
| 組織の体制 | 結果を読める人がいる、または 育てる時間がある |
読み手が点の数字しか求めない |
この表は、どちらかが優れているという表ではありません。同じ会社の中でも、需要予測は右寄り、新規出店の売上見込みは中央寄りというように、案件ごとに答えが変わります。案件ごとに毎回この確認を行うこと自体が、手法選択を属人的な好みから切り離す仕組みになります。
採用すると決めた場合でも、既存の分析をいきなり置き換えることは勧められません。現場は既存の数字で業務を回しており、突然表示される値が変わると、良し悪し以前に混乱が生じます。現実的な進め方は、既存の頻度論的な分析や機械学習モデルを止めずに、その横でベイズモデルを走らせる形です。運用の世界では、新しいモデルを本番に流さず結果だけを記録して比べる方式をシャドーモードと呼びます。既存の仕組みに影響を与えないまま、実データで挙動を確認できます。
併走の期間中に確認することは3つあります。1つ目は、点予測に直したときに既存モデルと大きく食い違わないことです。中央値や事後平均が既存の予測から極端に離れる場合、モデルの定式化かデータの前処理に問題がある可能性が高く、まずそこを調べます。2つ目は、示した区間が実際に当たっているかです。95%の信用区間を出しているなら、実績値がその区間に入る割合がおおむね95%に近づくはずで、大きく外れるなら区間が狭すぎるか広すぎることになります。3つ目は、計算が安定して終わるかです。ある週だけ収束しない、実行時間が急に伸びるといった挙動は、本番運用に移す前に把握しておく必要があります。
入れる場所の選び方も重要です。最初の適用先は、意思決定への効き目が大きく、かつ失敗しても影響範囲が限られる場所を選びます。全社の需要予測をいきなりベイズ化するのではなく、新商品カテゴリの需要見込みや、出店候補地の売上見込みのように、対象が絞られていて、しかも判断の金額が大きい場面から始めます。この選び方であれば、成功した場合は説得力のある事例が1つできますし、うまくいかなかった場合も既存業務は無傷です。
もう1つ、導入初期に決めておくとよいのが、事前分布の設定根拠を残す運用です。事前分布は前提の明示だと述べましたが、明示したものが文書に残っていなければ意味がありません。どの数値をどんな根拠で置いたのか、それを変えたときに結論がどう動くのかを記録します。前提を動かして結論の変化を見る手続きを感度分析といい、ベイズを導入する場合は必須の工程になります。事前分布を弱いものと強いものの2通りで走らせ、結論が変わらないなら「この結論はデータから出ており、前提に依存していない」と言えます。結論が変わるなら、その事実こそ報告すべき内容です。
ベイズモデルの運用で最初に問題になるのは計算時間です。MCMCはデータ量とパラメータ数に応じて時間がかかり、階層モデルや状態空間モデルでは数分から数時間に及ぶこともあります。この時間をどう扱うかは、業務のサイクルから逆算して決めます。
日次で結果が必要な業務であれば、夜間バッチで前日分を再推定し、翌朝には結果がそろっている形にします。週次の会議で使うのであれば、週末に実行しておけば十分で、計算時間が数時間かかっても支障はありません。問題になるのは、会議の場で条件を変えて再計算したいという要求です。この場合は、あらかじめ想定される条件で複数の結果を計算しておき、当日は参照するだけにするか、事後サンプルを保存しておいて、そこから条件付きの集計だけを高速に行う設計にします。事後分布は数千から数万のサンプルとして保存でき、そこからの集計は一瞬で終わるため、この分離は有効です。
再学習の頻度は、対象がどれくらいの速さで変わるかで決めます。商品の需要構造や設備の劣化の傾向は数か月単位でしか変わらないため、毎日再推定する必要はありません。一方で、キャンペーン中の反応率や感染症の流行状況のように日単位で変わるものは、頻繁な更新が必要です。ここでもベイズ更新の性質が使えて、全期間を毎回計算し直すのではなく、前回の事後分布を今回の事前分布として引き継ぐことで計算量を抑えられます。ただし引き継ぎを続けると、古い期間の影響が残り続けるため、構造が変わったときに追随できなくなります。定期的に全期間で計算し直す運用と組み合わせるのが安全です。
監視の項目も決めておきます。収束の指標が基準を超えていないか、事後分布の幅が急に広がっていないか、実績値が信用区間から外れる頻度が増えていないかを自動でチェックし、異常があれば通知します。実行が成功したかどうかだけを見る監視では、収束していないサンプルから作った結果がそのまま流れ続ける事故を防げません。
意思決定への接続部分も、コードとして書いておくと運用が安定します。事後サンプルから期待される損失を計算し、損失の小さい選択肢を選ぶという処理は、次のように短く書けます。判断の基準を人の頭の中ではなく式として残すことで、後から前提を見直せるようになります。
import numpy as np
# posterior_samples は MCMC で得たパラメータの事後サンプル(1次元配列)を想定する
# ここでは「対策を実施する」か「見送る」かを、期待される損失の小さい方で選ぶ
def expected_loss_act(cost_of_action):
"""対策を実施した場合。固定費用が確実に発生する代わりに超過分の損失は生じない"""
return float(cost_of_action)
def expected_loss_wait(samples, threshold, loss_per_unit):
"""見送った場合。閾値を超えた分だけ損失が発生し、その期待値を事後サンプルから取る"""
shortfall = np.maximum(samples - threshold, 0.0)
return float(loss_per_unit * shortfall.mean())
def choose(samples, threshold, cost_of_action, loss_per_unit):
loss_act = expected_loss_act(cost_of_action)
loss_wait = expected_loss_wait(samples, threshold, loss_per_unit)
return "実施" if loss_act < loss_wait else "見送り", loss_act, loss_wait
この計算で使っている考え方を期待損失といいます。それぞれの選択肢について、起こりうる結果ごとの損失を、その結果が起こる確率で重み付けして足し合わせた値です。点予測しか持っていない場合、この重み付けができないため、期待損失の比較は行えません。事後分布を持っていることの実務的な価値は、精度が上がることではなく、この比較が可能になることにあります。
ベイズ導入の成否は、モデルの出来よりも報告の作り方で決まる面があります。事後分布は情報量が多いため、そのまま見せると読み手が処理しきれません。報告では、意思決定に必要な形まで要約して渡します。
実務で使いやすい要約は、代表値、区間、そして閾値を超える確率の3点です。代表値は事後中央値か事後平均、区間は95%または80%の信用区間、確率は「目標を下回る確率」「損益分岐点を超える確率」のように、業務上意味のある線をまたぐ確率です。第3章で扱ったとおり、ベイズの信用区間は「この範囲に真の値が95%の確からしさで入る」という素直な読み方ができるため、説明の負担が小さくて済みます。
import numpy as np
def summarize(samples, threshold, level=95):
"""事後サンプルを、報告に使う3点セットに要約する"""
half = (100 - level) / 2
lower, upper = np.percentile(samples, [half, 100 - half])
return {
"median": float(np.median(samples)),
"interval": (float(lower), float(upper)),
"prob_over_threshold": float((samples > threshold).mean()),
}
報告文の書き方も、従来の様式から少し変える必要があります。対応関係を整理すると次のようになります。
| 伝えたい内容 | 従来の書き方 | ベイズでの書き方 | 受け手が取れる行動 |
|---|---|---|---|
| 売上の見込み | 来期売上は12.4億円と予測 | 中央値12.4億円 80%の確率で10.8〜14.1億円 |
下限でも成立する計画と 上振れ時の増産準備を分けて用意する |
| 施策の効果 | 有意差あり(p<0.05) | Bのほうが良い確率は93% 改善幅は0.5〜3.2ポイント |
93%で踏み切るか もう1週データを取るかを判断する |
| 設備の故障 | 故障確率は7% | 今月中の故障確率は7% 区間は3%〜14% |
最悪14%を前提に 予備部品の在庫を決める |
| 店舗別の実力 | A店の成約率は40% | A店の成約率は32% (データが少ないため全体平均へ寄せた値) |
件数が増えるまで 順位付けの判断を保留する |
区間の水準は95%にこだわらなくてかまいません。95%は統計学の慣行として広く使われますが、経営判断の場では区間が広くなりすぎて「幅がありすぎて使えない」という反応を招くことがあります。80%区間を主に見せ、95%区間を補足として添える形にすると、実務上の使い勝手と保守性のバランスが取りやすくなります。重要なのは、どの水準を使っているかを毎回同じ書式で明示することです。
図の作り方にも共通の型を決めておきます。時系列であれば、中央値の線に区間の帯を重ねる形が読みやすく、複数案の比較であれば、案ごとに区間を横棒で並べ、判断の閾値を縦線で入れると、どの案が線を越えているかが一目で分かります。図の様式を社内で統一しておくと、読み手は毎回の説明を必要とせず、数字の中身に集中できます。

幅のある数字を使うようになると、組織の側にも変化が生まれます。第一の変化は、計画の作り方です。点予測を前提にすると、計画は1本しか作れません。区間を前提にすると、下限に近い場合でも回る計画と、上限に近い場合に取りにいく計画を、あらかじめ両方用意する発想になります。これは楽観と悲観を並べる作業ではなく、それぞれの場合に何をするかを事前に決めておく作業です。決めてあれば、実際に振れたときの反応が早くなります。
第二の変化は、予測が外れたときの扱いです。点予測の文化では、外れたことは失敗として扱われ、担当者は外れにくい保守的な数字を出すようになります。その結果、予測は当たるが役に立たない数字になっていきます。区間の文化では、実績が区間の中に入っていれば予測は機能していたことになり、区間から外れた場合にはモデルの前提を疑うという建設的な話になります。評価の対象が担当者個人から、モデルと前提に移ります。
第三の変化は、データが足りないことを言葉にできるようになる点です。区間が広いという事実は、そのまま「この判断に必要な情報がまだ足りない」という報告になります。従来は、データが足りなくても何らかの点の数字を出すことが求められ、その数字の危うさは口頭の補足に押し込まれていました。区間で示せば、危うさが数字の中に含まれるため、追加のデータ取得に投資すべきか、それとも今の情報で決めるかという議論を、感覚ではなく幅の大きさに基づいて行えます。
第四の変化は、判断の記録が残ることです。期待損失で選んだのであれば、そのときの前提となる費用、損失、閾値がすべて数字として残ります。後から結果を振り返るとき、判断が間違っていたのか、前提が間違っていたのか、単に運が悪かったのかを切り分けられます。この切り分けができる組織は、同じ失敗を繰り返しにくくなります。
最後に、実務で見かけることの多いつまずき方を挙げておきます。いずれも技術的に難しい話ではなく、事前に決めておけば避けられるものです。
1つ目は、区間を出したのに結局は中央値だけが独り歩きする状態です。報告資料の本文に中央値だけを書き、区間を注釈に入れると、ほぼ確実にこうなります。本文の文中に区間を含めた文章として書く、資料のテンプレートに区間の欄を設けるといった、様式の側の工夫で防ぎます。
2つ目は、事前分布を強く置きすぎて結論が前提どおりになる状態です。結論が期待どおりに出たときほど、感度分析でその結論が事前分布に依存していないかを確認します。逆に、事前分布を弱くしておけば安全という考え方も適切ではありません。データが少ないときに無情報に近い事前分布を置くと、推定が発散したり、現実にはありえない値まで区間が広がったりします。過去の実績や物理的な上限下限から、常識的な範囲を指定するほうが結果は安定します。
3つ目は、収束の確認を省く運用です。第4章で扱った収束の診断指標は、結果を使ってよいかどうかの合否判定です。実務のバッチでは、この判定を通らなかった場合に結果を出力せず、通知して止める仕組みを最初から組み込みます。手作業で毎回確認する運用は、数か月で形骸化します。
4つ目は、モデルを複雑にしすぎる状態です。階層構造を何段にも重ね、相関構造を入れ、時間変動も入れると、計算は遅くなり、収束は不安定になり、説明もできなくなります。まず単純なモデルを動かして報告の型を作り、そのモデルでは説明できない現象が具体的に見つかったときに初めて要素を足していく順序が確実です。
5つ目は、担当者が1人しかいない状態です。ベイズモデルはコードを読めば分かるとは限らず、事前分布の選び方やモデルの構造に判断が含まれます。その判断を文書に残し、少なくとも2人が中身を理解している状態を作らないと、担当者の異動でモデルごと使えなくなります。運用に乗せる以上、引き継ぎ可能性は精度と同じくらい重要な要件です。
ベイズを使うかどうかは、データが少ないか、グループ構造があるか、不確実性が意思決定に直結するか、入れたい事前知識や構造があるか、逐次的に更新したいかという5つの条件で判断します。逆に、大量のデータで点予測の精度だけが問われる場合、リアルタイム性の制約が厳しい場合、結果を解釈して使える体制がない場合、検証の手立てがない場合、時間が極端に足りない場合は、無理に使う必要はありません。
導入は、既存の分析を止めずに横で走らせる形から始め、意思決定への効き目が大きく影響範囲の限られた場所を最初の適用先に選びます。運用では、計算時間を業務サイクルから逆算し、再学習の頻度を対象の変化速度に合わせ、収束と区間の当たり具合を自動で監視します。報告では、代表値と区間と閾値を超える確率の3点に要約し、様式を統一します。
そして、幅を持った数字を扱う文化そのものが、モデルの精度とは別の価値を生みます。計画を複数の場合に分けて準備できるようになり、外れたときの議論が前提の見直しに向かい、情報が足りないことを数字として言えるようになり、判断の記録が残ります。ベイズ統計を導入することの実務的な意味は、ここまで含めて考えたときにはっきりします。
『意思決定分析と予測の活用 基礎理論からPython実装まで』(馬場真哉、講談社):予測の結果を意思決定にどうつなげるかを、期待損失や情報の価値といった考え方から実装まで通して扱っています。本章で述べた「予測を判断に変換する部分」を体系的に学びたい場合に適しています。
『データ分析のための数理モデル入門 本質をとらえた分析のために』(江崎貴裕、ソシム):さまざまなモデリング手法の位置づけと、どの目的にどの手法が向くかを俯瞰的に整理した一冊です。ベイズを含む選択肢の中から手法を選ぶ視点を養うのに役立ちます。
本コラムでは、ベイズ統計の考え方と使い方を、基礎から応用まで通して見てきました。最後に、全体を貫いていた考え方を短く整理します。
ベイズ統計の技術要素は多岐にわたりますが、実務上の価値は「答えを分布で持つ」という一点に集約されます。分布で持つから、区間で報告できる。分布で持つから、損失と掛け合わせて期待損失で意思決定できる。分布で持つから、データが増えるたびに更新できる。MCMCも階層モデルも、この一点を実現するための手段です。
第7章の階層ベイズ、第9章の構造モデルとの組み合わせが示していたのは、データが少ないことは推定をあきらめる理由ではなく、事前知識や構造の知識を持ち込む理由になる、ということです。「この対象はデータが少ないので分析できません」という報告の少なくない部分は、ベイズの枠組みでは「少ないなりの幅で、いま言えることはここまでです」という報告に置き換えられます。
一方で、第12章で見たとおり、ベイズはすべての場面で優れているわけではありません。大量のデータで点予測の精度だけが問われる場面では、通常の機械学習の方が速くて強いことも多い。手法への愛着ではなく、意思決定の要件から採否を決めることが、この分野に限らない実務の作法だと考えています。
不確実性は、消すことのできない実務の条件です。ベイズ統計は、それを無視するのでも恐れるのでもなく、数字として扱えるようにする技術です。本コラムが、その最初の一歩と、二歩目以降の地図になれば幸いです。
Anagraftでは、AIプロジェクトの構想・課題設計から、データ分析・機械学習モデルの開発、AI人材の育成まで一貫したご支援を行っています。ベイズモデリングの実務適用のご相談も承っています。会社概要・ご支援内容の詳細は、以下の資料からご覧いただけます。
各章末で紹介した書籍の一覧です。コメントは紹介箇所の再掲です。