StatModeling Memorandum

StatModeling Memorandum

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

陽に解ける常微分方程式を使ったモデル

今回はデータの背後に簡単な(陽に解ける)常微分方程式で記述できるダイナミクスがあると仮定して、Stanでパラメータの推定を行いたいと思います。

状況として定期的に(例えば一年ごとに)サービスをリリースした場合を考えます。それらのサービスを使う総利用者数の時系列があるとします。そして各サービスともリリース直後は盛り上がってくるものの、じわじわと利用者が減っていくとします。今回はその利用者がどのサービス由来かわからないとして解析します(どのサービス由来か分かればもっと簡単にできます)。また今回のデータ取得期間はt=1:100でt=0,40,80に計3つの新規サービスをリリースしたと分かっているとし、グラフは以下のようになっているとします(縦軸の単位は[百人]とか)。

f:id:StatModeling:20201107121316p:plain

さてこの時系列を解析するうえで「リリース直後は盛り上がってくるものの、じわじわ減る」というダイナミクスをモデルに反映するのが大切となります。これをなるべくシンプルな常微分方程式で記述したいとすると、以下のような単純な流入項と流出項のある常微分方程式が考えられます。Xはまだサービスを使っていないが結果的に使うことになる利用者数、Yはある時刻における利用者数です。f:id:StatModeling:20201107121248p:plain流入速度係数, f:id:StatModeling:20201107121252p:plainは流出速度係数です。

f:id:StatModeling:20201107121245p:plain

これは陽に解けて、特に変数Yは以下の式になります。

f:id:StatModeling:20201107121256p:plain

f:id:StatModeling:20201107121301p:plainはある時刻の利用者数の上限に関係する定数です。例えばパラメータがf:id:StatModeling:20201107121305p:plainの時の変数Yの挙動は以下のような時系列になります。

f:id:StatModeling:20201107121319p:plain

この3パラメータ(f:id:StatModeling:20201107121308p:plain)の式を使ってStanでフィッティングしていきます。Stanコードは以下にな りました。

data {
   int<lower=1> N_times;
   real<lower=0> Y[N_times];
}
parameters {
   vector<lower=0, upper=1.0>[3] k_up;
   real<lower=0, upper=min(k_up)> k_down;
   vector<lower=0, upper=10>[3] a_const;
   real<lower=0, upper=10> alpha;
   real<lower=0> s_noise;
}
transformed parameters {
   vector<lower=0>[N_times] y_service[3];
   vector<lower=0>[N_times] y_hidden;
   for (t in 1:N_times){
      y_service[1,t] <- a_const[1]*(exp(-k_down*t) - exp(-k_up[1]*t));
      y_service[2,t] <- if_else(t > 40, a_const[2]*(exp(-k_down*(t-40)) - exp(-k_up[2]*(t-40))), 0.0);
      y_service[3,t] <- if_else(t > 80, a_const[3]*(exp(-k_down*(t-80)) - exp(-k_up[3]*(t-80))), 0.0);
      y_hidden[t] <- alpha + y_service[1,t] + y_service[2,t] + y_service[3,t];
   }
}
model {
   Y ~ normal(y_hidden, s_noise);
}
  • 6~8行目: 今回はk_up, a_const はサービスごとに異なるとし、k_down は各サービスで共通としました。
  • 7行目: k_up > k_down でないと「リリース直後は盛り上がってくるものの、じわじわ減る」という時系列にはなりませんのでその条件になります。
  • 16~18行目: 陽に解いた式をそのまま使用しています。過去のサービスの利用者も積み重なっていくのでこのようになります。

キックするRコードは省略します。データを渡す時に100で割って適度にスケーリングして渡しました。

結果は以下になりました。

meanse_meansdX2.5.X25.X50.X75.X97.5.n_effRhat
k_up[1]0.49 0.01 0.12 0.29 0.41 0.48 0.57 0.78 395 1.01
k_up[2]0.14 0.00 0.02 0.10 0.13 0.14 0.15 0.18 627 1.00
k_up[3]0.28 0.00 0.06 0.19 0.24 0.27 0.31 0.42 1009 1.00
k_down0.0064 0.0001 0.0022 0.0030 0.0049 0.0062 0.0076 0.0119 391 1.00
a_const[1]1.20 0.01 0.23 0.81 1.03 1.18 1.34 1.71 438 1.01
a_const[2]1.13 0.01 0.14 0.90 1.04 1.11 1.20 1.46 480 1.00
a_const[3]0.82 0.00 0.10 0.65 0.75 0.81 0.88 1.05 542 1.00
alpha0.78 0.01 0.24 0.24 0.64 0.81 0.95 1.19 384 1.01
s_noise0.094 0.000 0.007 0.081 0.089 0.093 0.098 0.108 1356 1.00

この記事の最後にデータを生成したRコードを載せていますが、真の値をかなりよく推定できています。

データ上の利用者数(黒の○点)、ノイズ項を除いた真の利用者数(黒の点線)、その推定されたMCMCサンプルの中央値(青の実線)、同95%信用区間(グレーの帯)をプロットしたものが以下になります。

f:id:StatModeling:20201107121324p:plain

また切片+3つのサービスの利用者数に分けて推定された値(MCMCサンプルの中央値)を積み重ね棒グラフでプロットしたものが以下になります。

f:id:StatModeling:20201107121311p:plain

ちなみにダイナミクスが複雑で常微分方程式が陽に解けない場合は、パラメータをMCMCで探索しながら微分方程式を解くといったことを繰り返す必要があり、StanではODEの機能として実装されています(Stanのマニュアルの「19. Solving Differential Equations」の章)。

最後にこの記事のデータを生成するRコードは以下でした。

N.times <- 100
times <- 1:N.times
k.up1 <- 0.34
k.up2 <- 0.12
k.up3 <- 0.25
k.down <- 0.008
A1 <- 1.0
A2 <- 1.2
A3 <- 0.8
s.true <- 0.1
set.seed(123)

y1.true <- A1*(exp(-k.down*times) - exp(-k.up1*times))
y2 <- A2*(exp(-k.down*times) - exp(-k.up2*times))
y3 <- A3*(exp(-k.down*times) - exp(-k.up3*times))
y2.true <- c(rep(0,40), y2[1:(N.times-40)])
y3.true <- c(rep(0,80), y3[1:(N.times-80)])

y.true <- 1.0 + y1.true + y2.true + y3.true
y <- 100 * (y.true + rnorm(N.times, 0, s.true))
save.image("explicit_ode.RData")