StatModeling Memorandum

StatModeling Memorandum

StanとRとPythonでベイズ統計モデリングします. たまに書評.

Bayesian GPLVMをStanで実装してみた

この記事の続きです。PRML下の12章に出てくるOil Flowのデータ(データ点1000個×特徴量12個)に対してBayesian GPLVMで2次元(または3次元)の潜在変数空間にマッピングして綺麗に分離されるか見てみます。

まずはPRMLにもあるように普通の主成分分析でやると以下になります。綺麗には分離されません。

f:id:StatModeling:20201106181957p:plain

次にBayesian GPLVMでやってみます。Stanコードは以下になります。

  • 2~4行目: NKDはそれぞれ、データ点の数・特徴量の数・最終的に落とし込む潜在空間の次元です。
  • 14行目: 潜在変数です。
  • 15行目: カーネルに含まれるパラメータです。僕が理解したところだと特徴量ごとにガウス過程が存在するのでKごとに異なる値を持つようにしています。→ 2017.07.02追記 Kごとに異なる値にするのではなく、1つだけにし、スケーリングしてから適用することで情報を圧縮させる方がふつうのようです。詳しくはMLPシリーズ『ガウス過程と機械学習』参照。
  • 19行目: 同様に特徴量ごとに分散共分散行列があります。
  • 20・22~23行目: カーネルの定義で効率的な行列演算をするため、matrix型をvector型の配列に持ち替えます。
  • 24~28行目: カーネルの定義です。ここではGaussian+bias+linear+white noiseにしました。カーネルについてはGP summser school 2015のKernel Designの講義資料 (pdf)The Kernel Cookbookなどを参照してください。
  • 29行目: 潜在空間に対する縛りです。代わりにparametersブロックでlowerupperを定めてもOKです。
  • 30~31行目: カーネルに含まれるパラメータの事前分布です。軽くしばっています。ある範囲の一様分布にするなど他にも色々考えられると思います。
  • 32~33行目: ガウス過程の部分です。この書き方だと各特徴量は独立になっていますが、さらに特徴量間の相関を考慮したモデル(例えばCoregionalized Regression Model)もあります。ここではすでに計算量が膨大なので独立としました。

Rからの実行方法は以下です。計算が重いのでADVIを使いました。

  • 5行目: データがmatlabのファイルで与えられていたのでそれを読み込んでいます。結果はリストになります。
  • 8行目: 潜在変数の次元Dを与えています。最終的に2次元にプロットしたいです。余裕をもって次元を設定し、主成分分析のように寄与の大きい次元トップ2だけを抽出することで2次元に射影する方法もあるようです。ここでははじめから2次元の空間とします。また3次元の空間も試して3Dプロットしてみます。
  • 10~16行目: まずはふつうの主成分分析しています。前述の図はここで出力しているresult-pca.pngになります。またGPLVMを実行する際の初期値にする意味もあります。
  • 18~28行目: Bayesian GPLVMを実行しています。21行目ではinitオプションでPCAの結果を初期値として設定しています。22行目では時間短縮のためetaを求めないで1で与えています(手元のモデルとデータではチューニングの結果がいつもeta = 1だったという理由があります)。

計算時間はAWS EC2のc4.xlargeを使っても3時間半ぐらいかかりました。かなり遅いです。ADVIの代わりにStanのoptimizing関数を使って推定した方がよいかもしれません。また汎用的な確率的プログラミング言語ではガウス過程に特化した専用ライブラリにはかないません。Stanのモデルはユーザの問題にあわせた拡張が簡単なので、その点で使う価値はあると思います。特にモデルを拡張する予定がないならば、もしくはデータが巨大ならば、Pythonガウス過程に特化したライブラリであるGPyなどの使い方を学ぶべきと思います。

結果

D = 2の場合

潜在変数xの乱数サンプルの中央値を使って2次元の散布図を描くと以下になりました(result-bgplvm.png)。それなりに綺麗に分離していると思います。乱数の種の影響も見ましたが、おおよそ似たような結果になりました。

f:id:StatModeling:20201106181953p:plain

D = 3の場合

同様に3次元の散布図を描きました。2次元の場合より若干綺麗に分離していそうです。

f:id:StatModeling:20201106181948p:plain

条件付き独立と有向分離を用いた統計モデルの妥当性チェック

この記事では条件付き独立と有向分離を使い、作成したモデルが背景知識と齟齬がないかチェックする方法を紹介します。以下の本の4章と5.3.1項を参考にしています。

Modeling and Reasoning with Bayesian Networks

僕が持っているのは上の一版なのですが、新しい二版が出ているみたいです。

Modeling and Reasoning with Bayesian Networks

Modeling and Reasoning with Bayesian Networks

  • 作者:Darwiche, Adnan
  • 発売日: 2014/08/07
  • メディア: ペーパーバック
以降では単にグラフと言えばDAG(閉路を持たない有向グラフ)を指すことにします。

条件付き独立とは

以降では確率変数の集合を太文字で \textbf{X},\textbf{Y},\textbf{Z}のように太文字で表します。確率変数が1つの場合は X,Y,Zのように表します。

条件付き独立とは「 \textbf{Z}の値が与えられると、 \textbf{X} \textbf{Y}と独立になる」ことを指します。 I(\textbf{X},\textbf{Z},\textbf{Y})で表現します。「\textbf{Z}が与えられると、\textbf{Y}の情報は\textbf{X}を推測する上でヒントにならない。」と表現することもできます。

「確率変数 \textbf{X} \textbf{Y}が独立」とは P(\textbf{x},\textbf{y})=P(\textbf{x}) P(\textbf{y})を意味するのでした。条件付き独立は以下を意味します。

  P(\textbf{x},\textbf{y}|\textbf{z}) = P(\textbf{x}|\textbf{z}) P(\textbf{y}|\textbf{z})

両辺を P(\textbf{y}|\textbf{z})で割ることで、以下の形で表現することもできます( P(\textbf{y},\textbf{z}) \neq 0とします)。

  P(\textbf{x}|\textbf{y},\textbf{z}) = P(\textbf{x}|\textbf{z})

あるグラフィカルモデル Gが与えられると、たちどころに各ノード Xについて  I(X, Parents(X), Non\_Descendants(X))が成り立ちます。ここで Parents(X) Xの親で、 Xへ直接矢印が向いているノードの集合です。Descendants(X)Xの子孫、すなわち Xから矢印の向きに沿ってたどりつけるノードの集合です。したがって Non\_Descendants(X)は子孫以外の集合です(ただし Parents(X)は除く)。

それでは上記以外の条件付き独立はグラフィカルモデル Gに含まれていないのでしょうか。いいえ、次に挙げる性質で他の条件付き独立を導くことができます。

条件付き独立の諸性質

ここでは4つだけ取り上げます。

性質の名前 解釈
対称
symmetry
 I(\textbf{X},\textbf{Z},\textbf{Y}) = I(\textbf{Y},\textbf{Z},\textbf{X})  \textbf{z}が与えられた場合、\textbf{y}の情報が\textbf{x}のヒントにならなければ、\textbf{x}の情報もまた\textbf{y}のヒントにならない。その逆も然り。
分解
decomposition
 I(\textbf{X},\textbf{Z},\textbf{Y} \cup \textbf{W})ならば
  I(\textbf{X},\textbf{Z},\textbf{Y})かつ
  I(\textbf{X},\textbf{Z},\textbf{W})
\{\textbf{y},\textbf{w}\}の情報が\textbf{x}のヒントにならなければ、その一部である\textbf{y}\textbf{w}\textbf{x}のヒントにならない。
弱結合
weak union
 I(\textbf{X},\textbf{Z},\textbf{Y} \cup \textbf{W})ならば
  I(\textbf{X},\textbf{Z} \cup \textbf{W},\textbf{Y})かつ
  I(\textbf{X},\textbf{Z} \cup \textbf{Y},\textbf{W})
\{\textbf{y},\textbf{w}\}の情報が\textbf{x}のヒントにならなければ、その一部である\textbf{w}が与えられても残りの\textbf{y}の情報はヒントにならない。\textbf{y}を与えた場合も同様。
縮約
contraction
 I(\textbf{X},\textbf{Z},\textbf{Y})かつ
 I(\textbf{X},\textbf{Z} \cup \textbf{Y},\textbf{W})ならば
  I(\textbf{X},\textbf{Z},\textbf{Y} \cup \textbf{W})
関連のない \textbf{y}が与えられたあとで\textbf{w}の情報が\textbf{x}のヒントにならなければ、はじめからそれらを合わせた\{\textbf{y},\textbf{w}\}の情報もヒントにならない。

備考

  • 合成(composition)

以下の「合成(composition)」は一般に成り立たない。縮約(contraction)との違いに注目。

  I(\textbf{X},\textbf{Z},\textbf{Y})かつ I(\textbf{X},\textbf{Z},\textbf{W})ならば I(\textbf{X},\textbf{Z},\textbf{Y} \cup \textbf{W})

  • 分解(decomposition)から導かれること

XをあるグラフGのノードとすると、すべての \textbf{W} \subseteq Non\_Descendants(X)に対し、 I(X,Parents(X),\textbf{W})が成り立つ。

  • 弱結合(weak union)から導かれること

XをあるグラフGのノードとすると、任意の \textbf{W} \subseteq Non\_Descendants(X)に対し、 I(X,Parents(X) \cup \textbf{W}, Non\_Descendants(X) \setminus \textbf{W})が成り立つ。

証明

各性質の証明です。興味なければ飛ばしてください。本に載っていなかったので僕が足したものです。間違っていたらすみません。

  • 対称(symmetry)の証明

 P(\textbf{x},\textbf{y}|\textbf{z}) = P(\textbf{x}|\textbf{z}) P(\textbf{y}|\textbf{z}) = P(\textbf{y}|\textbf{z}) P(\textbf{x}|\textbf{z}) = P(\textbf{y},\textbf{x}|\textbf{z})

  • 分解(decomposition)の証明

 P(\textbf{x},\textbf{y}|\textbf{z}) = \sum_{\textbf{w}} P(\textbf{x},\textbf{y},\textbf{w}|\textbf{z}) = \sum_{\textbf{w}} P(\textbf{x}|\textbf{z}) P(\textbf{y},\textbf{w}|\textbf{z}) = P(\textbf{x}|\textbf{z}) P(\textbf{y}|\textbf{z})

2つめの等号で仮定を使っています。

  • 弱結合(weak union)の証明

 P(\textbf{x},\textbf{y}|\textbf{z},\textbf{w}) = \frac{P(\textbf{x},\textbf{y},\textbf{w}|\textbf{z})}{P(\textbf{w}|\textbf{z})} = \frac{P(\textbf{x}|\textbf{z}) P(\textbf{y},\textbf{w}|\textbf{z})}{P(\textbf{w}|\textbf{z})} = P(\textbf{x}|\textbf{z}) P(\textbf{y}|\textbf{z},\textbf{w}) = P(\textbf{x}|\textbf{z},\textbf{w}) P(\textbf{y}|\textbf{z},\textbf{w})

2つめの等号で仮定を使っています。また、仮定に分解(decomposition)を適用するとI(\textbf{X},\textbf{Z},\textbf{W})、すなわち P(\textbf{x}|\textbf{z})=P(\textbf{x}|\textbf{z},\textbf{w})が成り立つので、それを最後の等号で使っています。

  • 縮約(contraction)の証明

 P(\textbf{x},\textbf{y},\textbf{w}|\textbf{z}) = P(\textbf{x},\textbf{w}|\textbf{y},\textbf{z}) P(\textbf{y}|\textbf{z}) = P(\textbf{x}|\textbf{y},\textbf{z}) P(\textbf{w}|\textbf{y},\textbf{z}) P(\textbf{y}|\textbf{z})  \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ \ = P(\textbf{x}|\textbf{y},\textbf{z}) P(\textbf{y},\textbf{w}|\textbf{z}) = P(\textbf{x}|\textbf{z}) P(\textbf{y},\textbf{w}|\textbf{z})

2つめの等号で2番目の仮定を使っています。最後の等号で1番目の仮定を使っています。

有向分離とは

グラフィカルモデルが大きくなると、あるノードの集まり \textbf{X} \textbf{Y}の条件付き独立を調べるのは大変になります。もっと簡単かつ効率的に条件付き独立を判定するにはどうすればいいでしょうか。解決策の一つが有向分離(d-separation)です。

交わりを持たない \textbf{X},\textbf{Y},\textbf{Z}があって、あるグラフGにおいて\textbf{X}\textbf{Y}\textbf{Z}によって有向分離(d-separated)されているとは、 \textbf{X} \textbf{Y}をつなぐすべてのパス(矢印の向きは無視してよい)が \textbf{Z}によってブロックされていることを指します。dsep(\textbf{X},\textbf{Z},\textbf{Y})と表記します。

ブロックの前に「バルブが閉まっている」の説明をします。以下の3つの場合にA_1A_2が独立になり、A_1A_2の間のバルブが閉まっていると言います。

  • A_1\rightarrow W\rightarrow A_2というパスの形において、 Wが与えられている場合
  • A_1\leftarrow W\rightarrow A_2というパスの形において、 Wが与えられている場合
  • A_1\rightarrow W\leftarrow A_2というパスの形において、 Wとその子孫が与えられていない場合

「ブロックされている」とはパスのどこかで、少なくとも1つのバルブが閉まっていることです。この定義によると、バルブがそもそもない場合(すなわち直接矢印で X \rightarrow Yとなっている場合)にはブロックされようがありません。

この部分の説明と証明はPRML下の8.2節が分かりやすいです。

実際の dsep(\textbf{X},\textbf{Z},\textbf{Y})かどうかの判定方法は以下の手順で行うのが簡便です。

  1.  \textbf{X} \cup \textbf{Y} \cup \textbf{Z}に含まれない葉ノード(矢印が出ていないノード)を取り除く。葉ノードへ入る矢印も除く。グラフが変わらなくなるまでこのステップを繰り返し適用する。
  2.  \textbf{Z}から出ている矢印を取り除く。
  3.  \textbf{X} \textbf{Y}をつなぐパス(矢印の向きは無視してよい)が存在しなければ dsep(\textbf{X},\textbf{Z},\textbf{Y})

そして有向分離と条件付き独立は以下の定理で結びつきます。

 dsep(\textbf{X},\textbf{Z},\textbf{Y}) ならば  I(\textbf{X},\textbf{Z},\textbf{Y})

なお、無向グラフではA_1 - W - A_2というパスの形において、 Wが与えられている場合がブロックとなります。つながっているかどうかだけ見ればよいので簡単です。

有向分離の練習

本のFig4.12から引用です。以下のDAGを考えます。

f:id:StatModeling:20201106182234p:plain

  • 例1

上のDAGにおいて、 \textbf{X}=\{A,S\} \textbf{Y}=\{X,D\} \textbf{Z}=\{B,P\}によって有向分離されているか考えます。

f:id:StatModeling:20201106182237p:plain

データが与えられた確率変数は黒丸に白文字で書くのが慣例なのでそうしています。それでは実際に手順を適用してみましょう。判定方法の手順1によりDを取り除きます。手順2によりP \rightarrow Xの矢印を除きます。すると、\textbf{X}\textbf{Y}をつなぐパスはありませんので dsep(\textbf{X},\textbf{Z},\textbf{Y})と判定できます。

  • 例2

上のDAGにおいて、 \textbf{X}=\{T,C\} \textbf{Y}=\{B\} \textbf{Z}=\{S,X\}によって有向分離されているか考えます。

f:id:StatModeling:20201106182241p:plain

同様に判定方法の手順1によりDを取り除きます。手順2によりS \rightarrow CS \rightarrow Bの矢印を除きます。すると、\textbf{X}\textbf{Y}をつなぐパスはありませんので dsep(\textbf{X},\textbf{Z},\textbf{Y})と判定できます。

作成したモデルが背景知識と齟齬がないかチェックする例

本のFig5.9からの引用です。病気の診断をするモデルを2つ考えます。

1つめは以下のようなDAGです。モデル1としましょう。

f:id:StatModeling:20201106182246p:plain

2つめは以下のようなDAGです。モデル2としましょう。

f:id:StatModeling:20201106182250p:plain

モデル1がよいのかモデル2がよいのか考えています。いろいろな判断基準があると思いますが、ここでは背景知識とも言える「条件付き独立」と照らし合わせて考えてみます。

  • 「風邪を引いている」と分かっているとします。モデル1では「熱があるか」と「喉が痛いか」は有向分離になりません。「熱があるか」分かれば(恐らく扁桃炎の可能性がアップして)「喉が痛いか」かどうかのヒントになり得ます。一方、モデル2では「コンディション」が分かっているとすると、「熱があるか」と「喉が痛いか」は有向分離となり、条件付き独立になります。
  • モデル1では何もわかっていない状態においても、「体が痛い」と「扁桃炎」、「体が痛い」と「風邪」はそれぞれ有向分離となっており、条件付き独立となります。従って「体が痛い」という情報は「扁桃炎か」どうか「風邪か」どうかのヒントになりません。モデル2では有向分離していないので、「体が痛い」という情報は(インフルエンザの可能性を高め)「扁桃炎」および「風邪」があるかどうかのヒントになり得ます(恐らくこれらの可能性を減らすでしょう)。
  • モデル1では何もわかっていない状態においても、「熱があるか」と「風邪か」は有向分離となっており、条件付き独立となります。従って「熱がない」という情報は「風邪か」どうかのヒントになりません。モデル2では有向分離していないので、「熱がない」という情報は恐らく「風邪である」可能性を高めるでしょう。

このように条件付き独立と有向分離の考え方は背景知識と照らし合わせて統計モデリングに活用することができます。

その他の参考文献

グラフィカルモデルには上記以外にも無向グラフ、I-MAP、Perfect MAP、ネットワーク構造推定などさまざまなトピックがあります。詳しく知りたい読者は上記で挙げた二冊の本を参照してください。

因果推論の身近な話やバックドア基準については上記で挙げた本には載ってませんので、岩波DS3を読むとよいと思います。他の話も分かりやすく、実験者や研究者にオススメです。

岩波データサイエンス Vol.3

岩波データサイエンス Vol.3

  • 発売日: 2016/06/10
  • メディア: 単行本(ソフトカバー)

Python(PyStan)で「StanとRでベイズ統計モデリング」の5.1節を実行する

StanのPythonバインディングであるPyStanが公開されて久しいですが、検索してもあんまり情報がヒットしません。ちょっと寂しいと思ったので、インストールやtraceplotの出力なども含めて、以下の本の5.1節「重回帰」の一部を実行してみました(ステマです)。

StanとRでベイズ統計モデリング (Wonderful R)

StanとRでベイズ統計モデリング (Wonderful R)

本自体の紹介は以前の記事を読んでいただければと思います。

インストール

Windows 7 64bit、Python 3系でのインストール手順を説明します。

以上でPyStanが動くようになります。その他のOSにつきましては公式ページを読んでください。ほぼ同様の手順であり、難しくありません。

なおこの記事で使用したバージョンは次の通りです。

  • Python 3.5.2 | Anaconda 4.2.0 (64-bit)
  • PyStan 2.12.0.0

モデルファイル(Stanファイル)の説明

本の5.1節では、「大学の授業の出席率」を応答変数とし、過去の「アルバイトが好きかどうかというアンケート結果」と過去の「テストの点数」を説明変数として重回帰を行います。サポートページであるGitHubリポジトリに各種ファイルを置いてあります。

データやStanファイルの説明は本に任せて、この記事ではPythonからの使い方に注力します。

Stanファイルのコンパイルとサンプリング

pystanを使ったStanファイルのコンパイルとサンプリングは以下のように行います。

import pandas as pd
from pystan import StanModel
import matplotlib.pyplot as plt
import pickle

d = pd.read_csv('input/data-attendance-1.txt')
d.Score /= 200
data = d.to_dict('list')
data.update({'N':len(d)})

stanmodel = StanModel(file='model/model5-3.stan')

# NUTS (No U-Turn Sampler)
fit_nuts = stanmodel.sampling(data=data, n_jobs=1)
mcmc_sample = fit_nuts.extract()
mu_est = mcmc_sample['mu']

# ADVI (Automatic Differentiation Variational Inference)
fit_vb = stanmodel.vb(data=data)
vb_sample = pd.read_csv(fit_vb['args']['sample_file'].decode('utf-8'), comment='#')
vb_sample = vb_sample.drop([0,1])
mu_est = vb_sample.filter(regex='mu\.\d+')

with open('output/model_and_result.pkl', 'wb') as f:
    pickle.dump(stanmodel, f)
    pickle.dump(fit_nuts, f)
  • 6~9行目: データを読み込んでpandasDataFrameを作り、そこからStanに渡せるようにdictionaryに変換しています。7行目ではデータのScoreをスケーリングしています。推定したいパラメータのスケールを同じぐらいに揃えるのが目的です(収束が改善する場合があります)。
  • 11行目: Stanファイルのコンパイルだけを行っています。初期値を変えたりiterを変えたりしてサンプリングをいろいろ試したい場合に便利です。
  • 14行目: NUTSというアルゴリズムでサンプリングを実行しています。デフォルトだとコア数の分だけ並列で実行されます。しかし、Windowsの場合は無限コンパイル地獄に落ちる不具合があるので、n_jobs=1で並列を止める必要があります。このページを参照。
  • 15~16行目: 結果からMCMCサンプルを入手する一例です。
  • 19~22行目: NUTSではなく、自動変分ベイズ(ADVI)で推定する場合です。結果の形式がまだ洗練されておらず、ファイルから読み込んでからゴミの行を除去する必要があります(簡単ですが)。
  • 24~26行目: コンパイルされたモデルと結果をpickleで保存しておきます。

実行速度は基本的にC++コンパイラに依存するだけなので、Rの場合(少し古いgcc)と大きな差はなさそうです。

収束の要約とtraceplotの保存

本の4.4.8項「収束診断をファイルへ出力する」に相当します。pystanのデフォルトのプロット関数を使ってfit_nuts.plot()のようにするとtraceplotを描くことができますが、4つのchainを線種を変えて描くのではなくて1つのchainにまとめて描いてしまったり、表示がつぶれたりする不満点があります。代わりの関数としては、PyMCの開発チームが作成中のmcmcplotlibに期待していますが、例も使い方も載ってないので躊躇してしまいます。ここでは自作でRの{coda}パッケージ風のtraceplotを、複数ページのpdfで作成することにしました。

import pickle
import sys
import numpy as np
import math
import matplotlib.pyplot as plt
import seaborn as sns
from matplotlib.backends.backend_pdf import PdfPages

with open('output/model_and_result.pkl', 'rb') as f:
    stanmodel = pickle.load(f)
    fit_nuts = pickle.load(f)

## summary
with open('output/fit-summary.txt', 'w') as f:
    f.write(str(fit_nuts))

## traceplot
palette = sns.color_palette()
ms = fit_nuts.extract(permuted=False, inc_warmup=True)
iter_from = fit_nuts.sim['warmup']
iter_range = np.arange(iter_from, ms.shape[0])
paraname = fit_nuts.sim['fnames_oi']
num_pages = math.ceil(len(paraname)/4)

with PdfPages('output/fit-traceplot.pdf') as pdf:
    for pg in range(num_pages):
        plt.figure()
        for pos in range(4):
            pi = pg*4 + pos
            if pi >= len(paraname): break
            plt.subplot(4, 2, 2*pos+1)
            plt.tight_layout()
            [plt.plot(iter_range + 1, ms[iter_range,ci,pi], color=palette[ci]) for ci in range(ms.shape[1])]
            plt.title(paraname[pi])
            plt.subplot(4, 2, 2*(pos+1))
            plt.tight_layout()
            [sns.kdeplot(ms[iter_range,ci,pi], color=palette[ci]) for ci in range(ms.shape[1])]
            plt.title(paraname[pi])
        pdf.savefig()
        plt.close()
  • 9~11行目: pickleで保存した結果を読み込んでいます。
  • 14~15行目: MCMCサンプルの要約(Rhatなどの収束診断を含む)をファイルに出力しています。
  • 18~40行目: traceplotを描いています。
    • 18行目: chainごとの色をseabornパッケージのデフォルトカラーパレットで決めています。このままですとchainの数が6を超えたらエラーが出ます(色が巡回するようにすればOKと思います)。
    • 19行目: extract関数はMCMCサンプルを取り出す関数ですが、デフォルトだと各chainを混ぜて、iterationの順番も混ぜて出力します。開発チームの想定と異なる使われ方を防ぐためだそうです(ここ参照)。traceplotをプロットするにはchainごとにiterationの順番を保持したままのMCMCサンプルが欲しいので、このように引数を指定しています。
    • 20~21行目: ここではRの{rstan}パッケージと同様に、warmup期間を除いた部分のtraceplotと密度関数を描くようにしました。もしwarmup期間も含めてプロットしたい場合にはiter_from = 0としてください。
    • 22~23行目: 複数ページのpdfを作るためにパラメータ数を取得しています。1ページに4つのパラメータを描きます。
    • 25~40行目: 残りはmatplotlibの話なのでここでは省略します。複数ページのpdfの作成方法は公式ページを参考にしました。

結果の図は以下になります(pdfの1ページ目です)。

f:id:StatModeling:20201106182522p:plain

実測値と予測値のプロット

本の5.1.8項「図によるモデルのチェック」に相当します。図5.3を描きます。横軸に実測値(データ)・縦軸に予測分布の中央値と予測区間をプロットしたグラフです。

このようなグラフを描くにはMCMCサンプルを集計したり加工したりする必要があります。岩波DS1のGitHubリポジトリ内に、sinhrksさんpandasのコミッタ)に書いてもらったコードがありますので、よくそれらを参考にしています(example1,example2,example3)。

Pythonコードは以下になりました。

import pickle
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import seaborn as sns

with open('output/model_and_result.pkl', 'rb') as f:
    stanmodel = pickle.load(f)
    fit_nuts = pickle.load(f)

d_ori = pd.read_csv('input/data-attendance-1.txt')
ms = fit_nuts.extract()

quantile = [10, 50, 90]
colname = ['p' + str(x) for x in quantile]
d_qua = pd.DataFrame(np.percentile(ms['y_pred'], q=quantile, axis=0).T, columns=colname)
d = pd.concat([d_ori, d_qua], axis=1)
d0 = d[d.A == 0]
d1 = d[d.A == 1]

palette = sns.color_palette()
fig = plt.figure()
ax = fig.add_subplot(1,1,1)
ax.plot([0,0.5], [0,0.5], 'k--', alpha=0.7)
ax.errorbar(d0.Y, d0.p50, yerr=[d0.p50 - d0.p10, d0.p90 - d0.p50],
    fmt='o', ecolor='gray', ms=10, mfc=palette[0], alpha=0.8, marker='o')
ax.errorbar(d1.Y, d1.p50, yerr=[d1.p50 - d1.p10, d1.p90 - d1.p50],
    fmt='o', ecolor='gray', ms=10, mfc=palette[1], alpha=0.8, marker='^')
ax.set_aspect('equal')
ax.set_xlim(0, 0.5)
ax.set_ylim(0, 0.5)
ax.set_xlabel('Observed')
ax.set_ylabel('Predicted')
fig.savefig('output/fig5-3.png')
  • 14~19行目: MCMCサンプルの分位点(ここでは10%点, 50%点, 90%点)を計算してDataFrameにしています。行や列をしぼるのがラクで、描くときに便利だからです。

残りはmatplotlibの話なのでここでは省略します。結果の図は以下になります。

f:id:StatModeling:20201106182514p:plain

MCMCサンプルの散布図行列

同じく本の5.1.8項「図によるモデルのチェック」の、図5.5に相当する図を描きます。Pythonで散布図行列を描くにはseabornパッケージのPairGridを使うのが便利です。ただし、上三角行列の部分はカスタマイズする必要があります。ここを参考にしました。

import pickle
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import seaborn as sns
import math
from matplotlib.patches import Ellipse
from scipy import stats

with open('output/model_and_result.pkl', 'rb') as f:
    stanmodel = pickle.load(f)
    fit_nuts = pickle.load(f)

d_ori = pd.read_csv('input/data-attendance-1.txt')
ms = fit_nuts.extract()

def corrfunc(x, y, **kws):
    r, _ = stats.spearmanr(x, y)
    ax = plt.gca()
    ax.axis('off')
    ellcolor = plt.cm.RdBu(0.5*(r+1))
    txtcolor = 'black' if math.fabs(r) < 0.5 else 'white'
    ax.add_artist(Ellipse(xy=[.5, .5], width=math.sqrt(1+r), height=math.sqrt(1-r), angle=45,
        facecolor=ellcolor, edgecolor='none', transform=ax.transAxes))
    ax.text(.5, .5, '{:.0f}'.format(r*100), color=txtcolor, fontsize=28,
        horizontalalignment='center', verticalalignment='center', transform=ax.transAxes)

d = pd.DataFrame({'b1':ms['b1'], 'b2':ms['b2'], 'b3':ms['b3'], 'sigma':ms['sigma'],
                  'mu1':ms['mu'][:,0], 'mu50':ms['mu'][:,49], 'lp__':ms['lp__']},
                  columns=['b1', 'b2', 'b3', 'sigma', 'mu1', 'mu50', 'lp__'])
sns.set(font_scale=2)
g = sns.PairGrid(d)
g = g.map_lower(sns.kdeplot, cmap='Blues_d')
g = g.map_diag(sns.distplot, kde=False)
g = g.map_upper(corrfunc)
g.fig.subplots_adjust(wspace=0.05, hspace=0.05)
for ax in g.axes.flatten():
    for t in ax.get_xticklabels():
        _ = t.set(rotation=40)
g.savefig('output/fig5-5.png')
  • 17~26行目: 上三角行列を作成しています。Spearmanの順位相関係数に応じて、楕円の太さ細さと色を変えています。数値は相関係数×100を表示しています。
  • 36行目: subplotの間の空白を狭くしています。空いていると少し見づらかったので。
  • 37~39行目: X軸ラベルを少し傾けています。PairGridなので少しややこしいです。

結果の図は以下になります。

f:id:StatModeling:20201106182518p:plain

まとめ

  • pystanを使ってPythonからStanを実行してみました。思ったより簡単です。
  • numpypandasMCMCサンプルと相性がよくて楽しいです。
  • 作図の部分はRを使う場合と同様で、自分である程度頑張る必要があります。matplotlib力が試されます。
  • 「StanとRでベイズ統計モデリング」をPythonで実行するのは十分可能と思いました。