生存時間解析をやってみる2(PyMC3)

masao-ds.hatenablog.com ベイズモデリングによる生存時間解析はこちらを参考にしてください。

1.やりたいこと/課題設定

ある工場ではとある製品を量産している。複数台の生産装置を使用して製品を量産しているが、装置の故障が多発し、製品を生産することができず機会損失が生じていた。そこで生産装置を別の故障時間が長いものに入れ替えることにした。ベイズモデリングによる生存時間により、入れ替え前と後の生産装置での1ヶ月稼働時(装置メンテナンスを1ヶ月と想定)の機会損失額の差を見積もりたい。

2.ベイズモデリングによる生存時間解析を用いた機会損失額の比較

2つの故障時間が異なる生産装置の故障時間データを作成し、ベイズモデリングによる生存時間解析を行い、故障時間をモデル化する。その後、1ヶ月間工場で納入している生産装置で生産を行う場合の機会損失額を2つの異なる生産装置で比較する。この際、工場への生産装置の納入数は50台、1日1台あたりの生産個数は100個、製品1個あたりの売上は100円とする。

2.1.データの生成

まず故障時間の異なる架空の故障データを作成する。

#旧装置
# ワイブル分布のパラメータ
shape_old = 2 # ワイブル分布の形状パラメータ
scale_old = 28.0  # ワイブル分布の尺度パラメータ

# ランダムな故障時間を生成
np.random.seed(42)  # 乱数のシードを設定(再現性のため)
failures_old = np.random.weibull(shape_old, size=100) * scale_old

#新装置
# ワイブル分布のパラメータ
shape_new = 3.0 # ワイブル分布の形状パラメータ
scale_new = 35.0  # ワイブル分布の尺度パラメータ

# ランダムな故障時間を生成
np.random.seed(42)  # 乱数のシードを設定(再現性のため)
failures_new = np.random.weibull(shape_new, size=100) * scale_new
plt.hist(failures_old, alpha=0.5)
plt.hist(failures_new, alpha=0.5)
plt.show()

変更後の生産装置の方が故障までの時間が長くなるように生成した。また、以下のように40日経過後のデータは打切りデータとして扱うことにした。

#40日よりも後を身観測データとする。
df_failures_old=pd.DataFrame(failures_old, columns=['Time'])
df_failures_old['censored'] = df_failures_old['Time'] >= 40
y_old = df_failures_old['Time'].values
censored_old = df_failures_old['censored'].values

df_failures_new=pd.DataFrame(failures_new, columns=['Time'])
df_failures_new['censored'] = df_failures_new['Time'] >= 40
y_new = df_failures_new['Time'].values
censored_new = df_failures_new['censored'].values

2.2.装置の生存時間解析

2.2.1.変更前の装置の生存時間解析

PyMC3を使用したベイズモデリングを行う。生存時間分布にはワイブル分布を仮定する。

def weibull_lccdf(x, alpha, beta):
    """ Log complementary cdf of Weibull distribution. """
    return -((x / beta) ** alpha)

#打ち切りなしデータ+打ち切りありデータ
with pm.Model() as model_2:
    alpha_sd = 10.0
    mu = pm.Normal("mu", mu=0, sigma=100)
    alpha_raw = pm.Normal("a0", mu=0, sigma=0.1)
    alpha = pm.Deterministic("alpha", tt.exp(alpha_sd * alpha_raw))
    beta = pm.Deterministic("beta", tt.exp(mu / alpha))

    y_obs = pm.Weibull("y_obs", alpha=alpha, beta=beta, observed=y_old[~censored_old])
    #事後対数尤度に制約としてつけ加える。この項を含めた事後対数尤度が最大になるようにMCMCでサンプリングする。
    y_cens = pm.Potential('y_cens', weibull_lccdf(y_old[censored_old], alpha, beta))

with model_2:
    trace_2 = pm.sample(target_accept=0.9, init="adapt_diag")

事後サンプリング結果をプロットする。

_ = pm.traceplot(trace_2)
_ = pm.plot_posterior(trace_2)

サマリーを確認。

pm.summary(trace_2)
    mean    sd  hdi_3%  hdi_97% mcse_mean   mcse_sd ess_bulk    ess_tail    r_hat
mu  5.626   0.497   4.644   6.535   0.020   0.014   637.0   651.0   1.01
a0  0.052   0.008   0.036   0.068   0.000   0.000   668.0   642.0   1.01
alpha   1.692   0.140   1.439   1.974   0.005   0.004   668.0   642.0   1.01
beta    27.813  1.812   24.422  31.111  0.034   0.024   2768.0  2268.0  1.00

2.2.2.変更後の装置の生存時間解析

同様にPyMC3を使用したベイズモデリングにより生存時間解析を行う。

with pm.Model() as model_3:
    alpha_sd = 10.0
    mu = pm.Normal("mu", mu=0, sigma=100)
    alpha_raw = pm.Normal("a0", mu=0, sigma=0.1)
    alpha = pm.Deterministic("alpha", tt.exp(alpha_sd * alpha_raw))
    beta = pm.Deterministic("beta", tt.exp(mu / alpha))

    y_obs = pm.Weibull("y_obs", alpha=alpha, beta=beta, observed=y_new[~censored_new])
    #事後対数尤度に制約としてつけ加える。この項を含めた事後対数尤度が最大になるようにMCMCでサンプリングする。
    y_cens = pm.Potential('y_cens', weibull_lccdf(y_new[censored_new], alpha, beta))

with model_3:
    trace_3 = pm.sample(target_accept=0.9, init="adapt_diag")

事後サンプリング結果をプロットする。

_ = pm.traceplot(trace_3)
_ = pm.plot_posterior(trace_3)

サマリーを確認する。

pm.summary(trace_3)
sd   hdi_3%  hdi_97% mcse_mean   mcse_sd ess_bulk    ess_tail    r_hat
mu  8.309   0.755   6.915   9.784   0.031   0.022   591.0   635.0   1.01
a0  0.084   0.009   0.067   0.101   0.000   0.000   585.0   615.0   1.01
alpha   2.319   0.206   1.898   2.693   0.009   0.006   585.0   615.0   1.01
beta    36.039  1.737   32.854  39.437  0.032   0.023   2977.0  2575.0  1.00

2.3.故障時間の比較

以下のようなワイブル分布において、特定の時刻までに生存している確率を算出する関数を定義する。

# cdf
def weibCumDist(x, a, b):
    return  np.exp(-(x / b) ** a)

続いて、故障時間の分布をプロットする。ベイズモデリングではワイブル分布のパラメータを確率的に決定するため、生存時間分布も幅を持った推定が可能になる。

#サンプル数
n_sim = 2000
t = np.linspace(0, 90 , 90)
weibCum_2 = np.zeros((t.shape[0], n_sim))
weibCum_3 = np.zeros((t.shape[0], n_sim))
np.random.seed(42)
rand_val = pd.Series(np.random.randint(1000, 4000, n_sim))

k = 0
for val in rand_val:
    weibCum_2[:, k] = weibCumDist(t, trace_2['alpha'][val], trace_2['beta'][val])
    weibCum_3[:, k] = weibCumDist(t, trace_3['alpha'][val], trace_3['beta'][val])
    k+=1

plt.plot(t, weibCum_2.mean(axis=1))
plt.fill_between(t, np.percentile(weibCum_2, 95, axis=1), np.percentile(weibCum_2, 5, axis=1), alpha=0.5, label='old')

plt.plot(t, weibCum_3.mean(axis=1))
plt.fill_between(t, np.percentile(weibCum_3, 95, axis=1), np.percentile(weibCum_3, 5, axis=1), alpha=0.5, label='new')
plt.legend()
plt.xlabel('Time[Day]')
plt.ylabel('Probability')
plt.show()

2.4.損失額の比較

最後に機会損失額の分布を比較する。

def predict_loss_sample(trace, rand_v, time_to):
    
    alpha_sim = trace['alpha'][rand_v]
    beta_sim = trace['beta'][rand_v]
    t_sim = pd.Series(np.linspace(0, int(time_to), int(time_to) + 1))
    F_t = weibCumDist(t_sim, alpha_sim, beta_sim)

    return F_t.values * 1000 * 100 #生存確率*生産個数*1個あたり売り上げ

n_sim = 50
loss_2 = np.zeros(n_sim)
loss_3 = np.zeros(n_sim)
np.random.seed(42)
rand_val = pd.Series(np.random.randint(0, 2000, n_sim))

k = 0 
for val in rand_val:
    loss_2[k] = sum(predict_loss_sample(trace_2, val, 30))
    loss_3[k] = sum(predict_loss_sample(trace_3, val, 30))
    k += 1
    

plt.hist(loss_2, alpha=0.5, label='old')
plt.hist(loss_3, alpha=0.5, label='new')
plt.legend()
plt.xlabel('Opportunity loss amount')
plt.ylabel('Density')
plt.show()

以下、機械損失額の分布の比較。

機会損失額の差の分布を見てみる。

生産装置を入れ替えることで2万円〜7万円の機会損失を抑制できることがわかる。ベイズモデリングによる生存時間解析では,このように確率的に生存時間を推定できるため、機会損失額を分布で予測することが可能である。