2022年6・7月号の「実証ビジネス・エコノミクス」の第8回「価格戦略をダイナミックに考える:シングルエージェント動学モデルの推定[応用編]」に付随するRコードの紹介になります。
本連載の内容、およびサンプルコード等の資料は、情報の提供のみを目的としていますので、運用につきましては十分にご確認をいただき、お客様ご自身の責任とご判断によって行ってください。これらの情報の運用結果により損害等が生じた場合でも、日本評論社および著者はいかなる責任を負うことはできませんので、ご留意ください。
今回のプログラムの作成に際して、島本幸典さん(東京大学大学院経済学研究科)にご尽力頂きました。この場を借りて御礼申し上げます。
# ワークスペースを初期化
rm(list = ls())
# パッケージを読み込む
require(tidyverse)
require(skimr)
require(fixest)
require(numDeriv)
require(kableExtra)
第8回では、第7回で生成した自動車の買い替え行動に関するデータを引き続き用いる。
# 第7回で生成したデータを読み込む。
data_gen <-
readr::read_csv('Chap8_data.csv')
## Rows: 120000 Columns: 5
## ── Column specification ────────────────────────────────────────────────────────
## Delimiter: ","
## dbl (5): consumer, period, action, price, mileage
##
## ℹ Use `spec()` to retrieve the full column specification for this data.
## ℹ Specify the column types or set `show_col_types = FALSE` to quiet this message.
data_gen %>% head(3)
## # A tibble: 3 × 5
## consumer period action price mileage
## <dbl> <dbl> <dbl> <dbl> <dbl>
## 1 1 481 0 2400 95
## 2 1 482 1 2100 95
## 3 1 483 0 2300 0
前回同様、状態変数とその番号の関係を示すデータフレームを作成する。
## Stateの作成
# 価格の状態変数
price_states <- seq(2000, 2500, by = 100)
# 走行距離の状態変数
mileage_states <- seq(0, 100, by = 5)
# 価格の状態変数の数
num_price_states <- length(price_states)
# 走行距離の状態変数の数
num_mileage_states <- length(mileage_states)
# 状態変数は価格と走行距離の状態変数のペア
# 従って状態変数の数は価格の状態変数の数と走行距離の状態変数の数の積となる
num_states <- num_price_states * num_mileage_states
# 価格、走行距離の状態変数の組み合わせ(p,m)を1つのデータフレームで表す
state_df <-
dplyr::tibble(
# stateにidを番号付ける
state_id = 1:num_states,
# 順番は (p,m) = (2000,0),(2100,0),...,(2500,0),(2000,5),(2100,5),...
price_id = rep(1:num_price_states, times = num_mileage_states),
mileage_id = rep(1:num_mileage_states, each = num_price_states),
price = rep(price_states, times = num_mileage_states),
mileage = rep(mileage_states, each = num_price_states)
)
# 下3行を表示
state_df %>% tail(3)
## # A tibble: 3 × 5
## state_id price_id mileage_id price mileage
## <int> <int> <int> <dbl> <dbl>
## 1 124 4 21 2300 100
## 2 125 5 21 2400 100
## 3 126 6 21 2500 100
# 分析のためにデータを加工する
data_gen <-
data_gen %>%
# 状態変数を表した列を追加
dplyr::left_join(state_df, by = c('price', 'mileage')) %>%
dplyr::group_by(consumer) %>%
# 遷移行列の推定で使うため、ラグ変数を追加
dplyr::mutate(lag_price_id = lag(price_id),
lag_mileage_id = lag(mileage_id),
lag_action = lag(action)) %>%
dplyr::ungroup()
生成したデータの記述統計を確認する。
# 生成したデータの要約統計
data_gen %>%
dplyr::select(price, mileage, action) %>%
skimr::skim() %>%
skimr::yank("numeric") %>%
dplyr::select(skim_variable, mean, sd, p0, p100)
Variable type: numeric
skim_variable | mean | sd | p0 | p100 |
---|---|---|---|---|
price | 2323.71 | 152.03 | 2000 | 2500 |
mileage | 33.24 | 23.83 | 0 | 100 |
action | 0.03 | 0.17 | 0 | 1 |
# 推定に必要なパラメタの設定
# 時間割引率
beta <- 0.99
# オイラー定数
Euler_const <- - digamma(1)
# 消費者は車を購入するかどうか決めるため選択肢の数は2つ
num_choice <- 2
第7回と同様に遷移行列をデータから推定する。 遷移行列を推定するコードは前回と全く同じである。 まず、走行距離の遷移行列を推定する。
# それぞれの確率が実現した観察の数を数える
num_cond_obs_mileage <-
data_gen %>%
# 1期目は推定に使えないため落とす
dplyr::filter(period != min(data_gen$period)) %>%
# t期の走行距離、t+1期の走行距離、t期の購買ごとにグループ化して、観察数を数える
dplyr::group_by(lag_mileage_id, mileage_id, lag_action) %>%
dplyr::summarise(num_cond_obs = n(),
.groups = 'drop') %>%
# 確率ごとに名前を割り当てる
dplyr::mutate(
cond_obs_mileage = case_when(
# 1 - kappa_1 - kappa_2 の場合
(
(lag_action == 0 &
between(lag_mileage_id, 1, 20) &
(lag_mileage_id == mileage_id)) |
(lag_action == 1 &
mileage_id == 1)
) ~ 'cond_obs_mileage1',
# kappa_1 の場合
(
(lag_action == 0 &
between(lag_mileage_id, 1, 19) &
(lag_mileage_id == mileage_id - 1)) |
(lag_action == 1 &
mileage_id == 2)
) ~ 'cond_obs_mileage2',
# kappa_2 の場合
(
(lag_action == 0 &
between(lag_mileage_id, 1, 19) &
(lag_mileage_id == mileage_id - 2)) |
(lag_action == 1 &
mileage_id == 3)
) ~ 'cond_obs_mileage3',
# kappa_1 + kappa_2 の場合
(
lag_action == 0 &
lag_mileage_id == 20 &
mileage_id == 21
) ~ 'cond_obs_mileage4',
TRUE ~ 'other'
)) %>%
# 'other' は推定には使わないため落とす
dplyr::filter(cond_obs_mileage != 'other') %>%
# 確率ごとにグループ化し、再度、観察の数を数える
dplyr::group_by(cond_obs_mileage) %>%
dplyr::summarise(num_cond_obs = as.numeric(sum(num_cond_obs)),
.groups = 'drop') %>%
dplyr::select(num_cond_obs) %>%
as.matrix()
# 最尤法の解析解により推定値を求める
kappa_est <- c()
kappa_est[1] <-
(num_cond_obs_mileage[2] *
(num_cond_obs_mileage[2] + num_cond_obs_mileage[3] + num_cond_obs_mileage[4])) /
((num_cond_obs_mileage[2] + num_cond_obs_mileage[3]) *
(num_cond_obs_mileage[1] + num_cond_obs_mileage[2] +
num_cond_obs_mileage[3] + num_cond_obs_mileage[4]))
kappa_est[2] <-
(num_cond_obs_mileage[3] *
(num_cond_obs_mileage[2] + num_cond_obs_mileage[3] + num_cond_obs_mileage[4])) /
((num_cond_obs_mileage[2] + num_cond_obs_mileage[3]) *
(num_cond_obs_mileage[1] + num_cond_obs_mileage[2] +
num_cond_obs_mileage[3] + num_cond_obs_mileage[4]))
次に、価格の遷移行列を推定する。
# それぞれの確率が実現した観察の数を数える
num_cond_obs_price <-
data_gen %>%
# 1期目は推定に使えないため落とす
dplyr::filter(period != min(data_gen$period)) %>%
# t期の価格、t+1期の価格ごとにグループ化して、観察数を数える
dplyr::group_by(lag_price_id, price_id) %>%
dplyr::summarise(num_cond_obs = n(),
.groups = 'drop') %>%
# 観察数を行列(num_price_states行の正方行列)に変換
# price_id (t+1期の価格) を横に広げる
tidyr::pivot_wider(names_from = "price_id",
values_from = "num_cond_obs") %>%
dplyr::select(!lag_price_id) %>%
as.matrix()
# 最尤法の解析解により推定値を求める
lambda_est_mat <-
num_cond_obs_price / rowSums(num_cond_obs_price)
lambda_est_mat
## 1 2 3 4 5 6
## [1,] 0.10273973 0.10388128 0.19931507 0.19874429 0.1921233 0.20319635
## [2,] 0.10074627 0.09916327 0.19222071 0.20952058 0.1991180 0.19923112
## [3,] 0.10191150 0.10086410 0.29714585 0.19780047 0.2038230 0.09845509
## [4,] 0.09703103 0.10031847 0.19359975 0.30331827 0.2055681 0.10016437
## [5,] 0.04924386 0.04994318 0.09886652 0.09840030 0.5007722 0.20277397
## [6,] 0.05057165 0.05123799 0.09826752 0.09931963 0.2030932 0.49751000
lambda_est <- as.vector(t(lambda_est_mat))[c(-1,-8,-15,-22,-29,-36)]
走行距離、価格の遷移行列を生成する関数を作成し、推定された遷移行列を取得する。
# パラメタを所与として走行距離の遷移行列を出力する関数を作成
gen_mileage_trans <- function(kappa){
# 走行距離が1,2段階上がる確率をパラメタとする
kappa_1 <- kappa[1]
kappa_2 <- kappa[2]
# 購買しなかった場合の遷移行列を作成
mileage_trans_mat_hat_not_buy <-
matrix(0, ncol = num_mileage_states, nrow = num_mileage_states)
for (i in 1:num_mileage_states) {
for (j in 1:num_mileage_states) {
if (i == j){
mileage_trans_mat_hat_not_buy[i,j] <- 1 - kappa_1 - kappa_2
} else if (i == j - 1) {
mileage_trans_mat_hat_not_buy[i,j] <- kappa_1
} else if (i == j - 2){
mileage_trans_mat_hat_not_buy[i,j] <- kappa_2
}
}
}
mileage_trans_mat_hat_not_buy[num_mileage_states - 1, num_mileage_states] <-
kappa_1 + kappa_2
mileage_trans_mat_hat_not_buy[num_mileage_states, num_mileage_states] <- 1
# 購買した場合の遷移行列を作成
# 購入した期では m=0 となるため次の期のmileageはそこから決まることに注意
mileage_trans_mat_hat_buy <-
matrix(1, nrow = num_mileage_states, ncol = 1) %*%
mileage_trans_mat_hat_not_buy[1,]
# 3次元のarrayとして出力
return(array(c(mileage_trans_mat_hat_not_buy,
mileage_trans_mat_hat_buy),
dim=c(num_mileage_states,num_mileage_states,num_choice)))
}
# パラメタを所与として価格の遷移行列を出力する関数を作成
gen_price_trans <- function(lambda){
lambda_11 <- 1 - lambda[1] - lambda[2] - lambda[3] - lambda[4] - lambda[5]
lambda_22 <- 1 - lambda[6] - lambda[7] - lambda[8] - lambda[9] - lambda[10]
lambda_33 <- 1 - lambda[11] - lambda[12] - lambda[13] - lambda[14] - lambda[15]
lambda_44 <- 1 - lambda[16] - lambda[17] - lambda[18] - lambda[19] - lambda[20]
lambda_55 <- 1 - lambda[21] - lambda[22] - lambda[23] - lambda[24] - lambda[25]
lambda_66 <- 1 - lambda[26] - lambda[27] - lambda[28] - lambda[29] - lambda[30]
price_trans_mat_hat <-
c(lambda_11, lambda[1], lambda[2], lambda[3], lambda[4], lambda[5],
lambda[6], lambda_22, lambda[7], lambda[8], lambda[9], lambda[10],
lambda[11], lambda[12], lambda_33, lambda[13], lambda[14], lambda[15],
lambda[16], lambda[17], lambda[18], lambda_44, lambda[19], lambda[20],
lambda[21], lambda[22], lambda[23], lambda[24], lambda_55, lambda[25],
lambda[26], lambda[27], lambda[28], lambda[29], lambda[30], lambda_66) %>%
matrix(ncol = num_price_states, nrow = num_price_states, byrow=T)
return(price_trans_mat_hat)
}
# 推定された遷移行列を取得(本誌のG(i)に対応)
# 配列の第三次元が購買を意味しており、i=0,1という順番で並んでいる。
G <- array(c(gen_mileage_trans(kappa_est)[,,1] %x% gen_price_trans(lambda_est),
gen_mileage_trans(kappa_est)[,,2] %x% gen_price_trans(lambda_est)),
dim=c(num_states, num_states, num_choice))
今回はロジットモデルによる近似で、CCPを推定する。
# 単純なロジットモデルを推定する
logit_model <- fixest::feglm(action ~ price + price^2 + mileage + mileage^2,
data_gen, family = binomial("logit"))
# logit_model <- feglm(action ~ price + price^2 +
# mileage + mileage^2 +
# price*mileage + (price*mileage)^2
# , data_gen, family = binomial("logit"))
# logit_model_glm <- glm(action ~ price + price^2 + mileage + mileage^2,
# data_gen, family = binomial("logit"))
# 推定値に基づいてCCPを予測する。列はi=0,1という順番。
CCP_1st <-
cbind(
1 - predict(logit_model, state_df),
predict(logit_model, state_df)
)
以下のコードでは、二段階目の方法として紹介のあった (1) 行列形式によるインバージョン、と (2) 有限依存性 (finite dependence) アプローチの2つの手法を実装する。
# 前回同様、状態変数、コントロール変数毎の今期の効用を返す関数を作成
flow_utility <- function(theta, state_df){
theta_c <- theta[1]
theta_p <- theta[2]
U <-
cbind(
# その期における車を購入しない場合の効用
U_not_buy = - theta_c * state_df$mileage,
# その期における車を購入する場合の効用
U_buy = - theta_p * state_df$price
)
return(U)
}
# 前回同様、行列の(i,j)要素を出力する関数を作成
mat_ij <- Vectorize(
function(i,j,mat) {mat[i,j]},
vectorize.args = c("i", "j"))
# 行列インバージョンで期待価値関数を導出し、CCPを出力する関数を作成
policy_operator_mat_inv <- function(theta, CCP, beta, G, state_df){
# 今期の効用を計算(本誌のu(i)に対応)
U <- flow_utility(theta, state_df)
# psiを計算(本誌のpsi(i)に対応)
num_states <- nrow(state_df)
psi <- Euler_const * matrix(1, num_states, num_choice) - log(CCP)
# 行列計算により期待価値関数ベクトルを計算
V <-
solve(diag(num_states) -
beta * (CCP[,1] %*% matrix(1, 1, num_states) * G[,,1] +
CCP[,2] %*% matrix(1, 1, num_states) * G[,,2])) %*%
rowSums(CCP * (U + psi))
# 選択肢ごとの価値関数を計算
CV <- U + beta * cbind(G[,,1] %*% V, G[,,2] %*% V)
# CCPを更新
CCP <- exp(CV) / rowSums(exp(CV))
return(CCP)
}
likelihood_fun <- function(theta, CCP, df, beta, G, state_df, policy_operator){
# CCP operaterにより、CCPを更新する
# 今回は行列インバージョンと有限依存性(finite dependence)アプローチのどちらか
CCP <- policy_operator(theta, CCP, beta, G, state_df)
# 疑似尤度を計算
obj <- sum(log(mat_ij(df$state_id, df$action + 1, CCP)))
return(obj)
}
# パラメタの真の値
theta_true <- c(theta_c = 0.004, theta_p = 0.003)
start_time_mat_inv <- proc.time()
# 最適化
mat_inv_opt_mat_inv <- optim(theta_true, likelihood_fun,
CCP = CCP_1st, df = data_gen,
beta = beta, G = G, state_df = state_df,
policy_operator = policy_operator_mat_inv,
control = list(fnscale = -1),
method = "Nelder-Mead")
end_time_mat_inv <- proc.time()
run_time_mat_inv <- (end_time_mat_inv - start_time_mat_inv)[[3]]
print(stringr::str_c("Runtime: ", round(run_time_mat_inv,2)))
## [1] "Runtime: 15.08"
# 結果の確認
theta_mat_inv <- mat_inv_opt_mat_inv$par
theta_mat_inv
## theta_c theta_p
## 0.003884738 0.002971597
# 前回と同様、標準誤差を推定する
hessian <- numDeriv::hessian(func = likelihood_fun, x = theta_mat_inv,
CCP = CCP_1st, df = data_gen,
beta = beta, G = G, state_df = state_df,
policy_operator = policy_operator_mat_inv)
theta_se_mat_inv <- sqrt(diag(solve(-hessian)))
dplyr::tibble(theta_mat_inv, theta_se_mat_inv)
## # A tibble: 2 × 2
## theta_mat_inv theta_se_mat_inv
## <dbl> <dbl>
## 1 0.00388 0.0000846
## 2 0.00297 0.0000358
# 行列インバージョンで期待価値関数を導出し、CCPを出力する関数を作成
policy_operator_finite_dep <- function(theta, CCP, beta, G, state_df){
# 今期の効用を計算(本誌のu(i)に対応)
U <- flow_utility(theta, state_df)
# Finite dependenceに基づき、選択ごとの価値関数の差を計算
CV_dif <-
U[,2] - U[,1] +
beta * (G[,,2] %*% (-log(CCP[,2])) - G[,,1] %*% (-log(CCP[,2])))
# CCPを更新
prob_buy <- exp(CV_dif) / (1 + exp(CV_dif))
CCP <- cbind(1-prob_buy, prob_buy)
return(CCP)
}
start_time_finite_dep <- proc.time()
# 最適化
finite_dep_opt <- optim(theta_true, likelihood_fun,
CCP = CCP_1st, df = data_gen,
beta = beta, G = G, state_df = state_df,
policy_operator = policy_operator_finite_dep,
control = list(fnscale = -1),
method = "Nelder-Mead")
end_time_finite_dep <- proc.time()
run_time_finite_dep <- (end_time_finite_dep - start_time_finite_dep)[[3]]
print(stringr::str_c("Runtime: ", round(run_time_finite_dep,2)))
## [1] "Runtime: 9.79"
# 結果の確認
theta_finite_dep <- finite_dep_opt$par
theta_finite_dep
## theta_c theta_p
## 0.003821963 0.002903782
# 標準誤差を推定する
hessian <- numDeriv::hessian(func = likelihood_fun, x = theta_finite_dep,
CCP = CCP_1st, df = data_gen,
beta = beta, G = G, state_df = state_df,
policy_operator = policy_operator_finite_dep)
theta_se_finite_dep <- sqrt(diag(solve(-hessian)))
dplyr::tibble(theta_finite_dep, theta_se_finite_dep)
## # A tibble: 2 × 2
## theta_finite_dep theta_se_finite_dep
## <dbl> <dbl>
## 1 0.00382 0.000793
## 2 0.00290 0.0000219
以下は、本誌第8回第3節のノーテーションに合わせたコードになっている点に注意されたい。 そのため、前回(第7回)と比べると若干コードに変更点があるが、本質的には全く同じである。 当然、推定値も同じ結果が得られている。
contraction <-
function(theta, beta, G, state_df) {
# 価値関数の初期値
num_states <- nrow(state_df)
V_old <- matrix(0, nrow = num_states, ncol = 1)
# パラメタより今期の効用を計算
U <- flow_utility(theta, state_df)
# 価値関数の差の初期値
diff <- 1000
# 縮小写像の誤差範囲
tol_level <- 1.0e-10
while (diff > tol_level) {
# 価値関数を計算
V_new <- log(rowSums(exp(U + beta * cbind(G[,,1] %*% V_old, G[,,2] %*% V_old)))) + Euler_const
# 価値関数の更新による差を評価
diff <- sum(abs(V_new-V_old))
# 価値関数を次のループに渡す(価値関数の更新)
V_old <- V_new
}
# 得られた事前の価値関数を出力
return(V_old)
}
policy_operator_nfxp <-
function(theta, beta, G, state_df) {
# パラメタより今期の効用を計算
U <- flow_utility(theta, state_df)
# 縮小写像アルゴリズムで得られた事前の価値関数をVとする
V <- contraction(theta, beta, G, state_df)
# 選択肢ごとの価値関数を計算
CV <- U + beta * cbind(G[,,1] %*% V, G[,,2] %*% V)
# CCPを計算
CCP <- exp(CV) / rowSums(exp(CV))
return(CCP)
}
likelihood_fun_nfxp <- function(theta, df, beta, G, state_df){
# CCP operaterにより、
CCP <- policy_operator_nfxp(theta, beta, G, state_df)
# 疑似尤度を計算
obj <- sum(log(mat_ij(df$state_id, df$action + 1, CCP)))
return(obj)
}
start_time_nfxp <- proc.time()
# 最適化
nfxp_opt <- optim(theta_true, likelihood_fun_nfxp,
df = data_gen,
beta = beta, G = G, state_df = state_df,
control = list(fnscale = -1),
method = "Nelder-Mead")
end_time_nfxp <- proc.time()
run_time_nfxp <- (end_time_nfxp - start_time_nfxp)[[3]]
print(stringr::str_c("Runtime: ", round(run_time_nfxp,2)))
## [1] "Runtime: 37.08"
theta_nfxp <- nfxp_opt$par
theta_nfxp
## theta_c theta_p
## 0.003887129 0.002971684
# 標準誤差を推定する
hessian <- numDeriv::hessian(func = likelihood_fun_nfxp, x = theta_finite_dep,
df = data_gen,
beta = beta, G = G, state_df = state_df)
theta_se_nfxp <- sqrt(diag(solve(-hessian)))
dplyr::tibble(theta_nfxp, theta_se_nfxp)
## # A tibble: 2 × 2
## theta_nfxp theta_se_nfxp
## <dbl> <dbl>
## 1 0.00389 0.0000854
## 2 0.00297 0.0000348
dplyr::tibble(
algorithm = c('NFXP', 'Matrix', 'Finite', 'True'),
theta_c = c(theta_nfxp[1], theta_mat_inv[1], theta_finite_dep[1], theta_true[1]),
theta_se_c = c(theta_se_nfxp[1], theta_se_mat_inv[1], theta_se_finite_dep[1], NA),
theta_p = c(theta_nfxp[2], theta_mat_inv[2], theta_finite_dep[2], theta_true[2]),
theta_se_p = c(theta_se_nfxp[2], theta_se_mat_inv[2], theta_se_finite_dep[2], NA),
run_time = c(run_time_nfxp, run_time_mat_inv, run_time_finite_dep, NA)
) %>%
kableExtra::kable(format = "html",
booktabs = TRUE,
caption = "推定方法の比較",
digits = c(0,6,6,6,6,2),
format.args = list(scientific = FALSE)) %>%
kableExtra::kable_styling()
algorithm | theta_c | theta_se_c | theta_p | theta_se_p | run_time |
---|---|---|---|---|---|
NFXP | 0.003887 | 0.000085 | 0.002972 | 0.000035 | 37.08 |
Matrix | 0.003885 | 0.000085 | 0.002972 | 0.000036 | 15.08 |
Finite | 0.003822 | 0.000793 | 0.002904 | 0.000022 | 9.79 |
True | 0.004000 | NA | 0.003000 | NA | NA |
以下の二つのシナリオについてそれぞれCCPを計算し、結果を比較する。
# 以降のシミュレーションで得られたCCPを要素とするリストを作成
CCP_list <- list()
# シナリオ1) 推定した際と全く同じ状況
CCP_list[['Baseline']] <-
policy_operator_nfxp(theta_nfxp, beta, G, state_df)
# シナリオ1における、各価格の購買確率を計算
result_df_edlp <-
data_gen %>%
# データよりそれぞれの状態変数が観察された数を数える
dplyr::group_by(state_id, price_id, price) %>%
dplyr::summarise(num_obs = n(),
.groups = 'drop') %>%
dplyr::arrange(state_id) %>%
# シナリオ1で得られたCCPを新たな列に加える
dplyr::mutate(prob_buy_baseline = CCP_list[['Baseline']][,2]) %>%
# 走行距離について、購買確率の加重平均を取る(走行距離の頻度について積分する)
dplyr::group_by(price_id, price) %>%
dplyr::summarize(prob_buy_baseline = weighted.mean(prob_buy_baseline, num_obs),
.groups = 'drop')
# シナリオ2) 価格が最も低い価格に永続的に固定される状況
# このシナリオでは価格の変動がないため、走行距離についてのみの遷移行列がこのモデルの遷移行列となる
G_fixed_price <- gen_mileage_trans(kappa_est)
# 価格を固定した場合のCCPを計算
# 後ほどの需要と収入の計算で使うため、価格を固定した場合のCCPをすべての価格に対して計算
for (fixed_price in price_states) {
# 価格が固定された場合の、状態変数を示すデータフレームを作成
state_df_fixed_price <-
state_df %>%
dplyr::filter(price == fixed_price) %>%
dplyr::arrange(mileage_id)
# 遷移行列と状態変数を示すデータフレームが変わっていることに注意してCCPを計算
CCP_list[[stringr::str_c('edlp', fixed_price)]] <-
policy_operator_nfxp(theta = theta_nfxp, beta = beta,
G = G_fixed_price, state_df = state_df_fixed_price)
}
# 200万円に価格を固定した場合の購買確率を計算
prob_buy_edlp2000 <-
data_gen %>%
# データよりそれぞれの状態変数が観察された数を数える
dplyr::group_by(mileage_id, mileage) %>%
dplyr::summarise(num_obs = n(),
.groups = 'drop') %>%
dplyr::arrange(mileage_id) %>%
# 価格が固定された場合のCCPを新たな列に加える
dplyr::mutate(prob_buy = CCP_list[['edlp2000']][,2]) %>%
# 走行距離について、購買確率の加重平均を取る(走行距離の頻度について積分する)
dplyr::summarize(prob_buy = weighted.mean(prob_buy, num_obs)) %>%
as.matrix()
# 価格ごとの購買確率を図示する
result_df_edlp %>%
ggplot2::ggplot(aes(x = price, y = prob_buy_baseline)) +
ggplot2::geom_bar(stat = "identity") +
ggplot2::geom_hline(yintercept = prob_buy_edlp2000,
color = "blue", size = 1.0) +
ggplot2::annotate("text",
x = 2500, y = prob_buy_edlp2000 + 0.002,
color= "blue",
label = str_c("EDLP: ", round(prob_buy_edlp2000,5))) +
ggplot2::theme_minimal()
以下の3つのシナリオを比較することで、値下げの効果を分析する。
まず、それぞれのシナリオにおけるCCPを計算する。
# シナリオ2) 10万円の永続的な値下げ
# priceを100下げた、state_dfを用意し、CCPを再計算する
state_df_discount <-
state_df %>%
dplyr::mutate(price = price - 100)
CCP_list[['Permanent']] <-
policy_operator_nfxp(theta_nfxp, beta, G, state_df_discount)
# シナリオ3) 10万円の一時的な値下げ
# 値下げが起きたときの今期の効用を計算
U_discount <- flow_utility(theta_nfxp, state_df_discount)
# 元の価格設定における事前の価値関数を計算
V <- contraction(theta_nfxp, beta, G, state_df)
# 選択肢ごとの価値関数を計算
CV_temporary <- U_discount + beta * cbind(G[,,1] %*% V, G[,,2] %*% V)
# CCPを計算
CCP_list[['Temporary']] <-exp(CV_temporary) / rowSums(exp(CV_temporary))
# 三つのシナリオについて、元々の価格が220万円の場合の、走行距離ごとの購入確率を図示
state_df %>%
# 得られたCCPを一つのdataframeにまとめる
dplyr::mutate(ProbBaseline = CCP_list[['Baseline']][,2],
ProbPermanent = CCP_list[['Permanent']][,2],
ProbTemporary = CCP_list[['Temporary']][,2]) %>%
# 220万円での購買確率に絞る
dplyr::filter(price == 2200) %>%
dplyr::select(mileage, ProbBaseline,
ProbPermanent, ProbTemporary) %>%
tidyr::pivot_longer(cols = - mileage) %>%
dplyr::rename(scenario = name, prob_buy = value) %>%
ggplot2::ggplot(aes(x = mileage, y = prob_buy, color = scenario)) +
ggplot2::geom_line() +
ggplot2::geom_point() +
ggplot2::theme_minimal()
最初の期における、状態変数ごとの消費者の分布を所与とした時に、条件付き選択確率(CCP)と遷移行列を使えば、時間を通じた消費者の分布の遷移をシミュレートできる。 また、その分布とCCPを使うことで、各期の購買割合や収入がわかる。 これらの計算を用いて、値下げがデータ上の購買割合に与える影響や、需要と収入への影響を観察する。
# データから状態変数毎の消費者の分布を計算
# state_idの順番に並んだ、(1 * num_states) の行ベクトル
consumer_dist_obs <-
data_gen %>%
dplyr::group_by(state_id, price_id, mileage_id) %>%
dplyr::summarise(num_obs = n(),
.groups = 'drop') %>%
dplyr::mutate(consumer_dist_obs = num_obs/sum(num_obs)) %>%
dplyr::arrange(state_id) %>%
dplyr::select(consumer_dist_obs) %>%
as.matrix() %>%
t()
# 購買を考慮した遷移行列を作成
discount_scenerio_vec <- c('Baseline','Permanent','Temporary')
# CCPを考慮した遷移行列を要素とするリストを作成
G_CCP_list <- list()
# それぞれの値下げシナリオに応じた、購買を考慮した遷移行列を作成
for (scenario in discount_scenerio_vec) {
G_CCP_list[[scenario]] <-
(CCP_list[[scenario]][,1] %*% matrix(1, 1, num_states)) * G[,,1] +
(CCP_list[[scenario]][,2] %*% matrix(1, 1, num_states)) * G[,,2]
}
# 需要と収入の計算に使うため、EDLPを行ったときの購買を考慮した遷移行列も作成
for (scenario in stringr::str_c('edlp', price_states)) {
G_CCP_list[[scenario]] <-
(CCP_list[[scenario]][,1] %*% matrix(1, 1, nrow(G_fixed_price[,,1]))) * G_fixed_price[,,1] +
(CCP_list[[scenario]][,2] %*% matrix(1, 1, nrow(G_fixed_price[,,1]))) * G_fixed_price[,,2]
}
# シミュレーションする消費者数と期間を定義
num_consumer_sim <- 1000
num_period_sim <- 20
# 値下げによるシミュレーション結果を表すデータフレームを前もって作成
discount_sim_df <-
dplyr::tibble(period = 1:num_period_sim)
# それぞれのシナリオについてシミュレーションを行う
for (scenario in discount_scenerio_vec) {
# 消費者の分布の推移を表す行列を前もって作成
consumer_dist_sim <- matrix(0, num_period_sim, num_states)
# 初期の分布をデータ上の分布とする
consumer_dist_sim[1,] <- consumer_dist_obs
# 購買割合、需要、収入の空の列を作成
discount_sim_df <-
discount_sim_df %>%
dplyr::mutate(
prob_buy_sim = NA,
demand = NA,
revenue = NA
)
# t期における消費者の分布、購買割合、需要、収入を計算
for (t in 1:num_period_sim) {
# 初期、かつ、一時的な値下げの場合にのみ、一時的な値下げのCCPを参照する
if (t == 1 & scenario == 'Temporary'){
# 分布の推移
# 注意: 最終期は計算する必要がない
if (t != num_period_sim){
consumer_dist_sim[t+1,] <-
consumer_dist_sim[t,] %*% G_CCP_list[['Temporary']]
}
# 購買割合の計算
discount_sim_df$prob_buy_sim[t] <-
consumer_dist_sim[t,] %*% CCP_list[['Temporary']][,2]
# 需要量の計算
discount_sim_df$demand[t] <-
discount_sim_df$prob_buy_sim[t] * num_consumer_sim
# 収入の計算
discount_sim_df$revenue[t] <-
state_df %>%
dplyr::mutate(consumer_dist = consumer_dist_sim[t,],
CCP = CCP_list[['Temporary']][,2],
revenue = (price - 100) * consumer_dist * CCP * num_consumer_sim) %>%
dplyr::summarise(revenue = sum(revenue)) %>%
as.matrix()
} else{
# 実際に消費者が従うCCPのシナリオ名を得る
# 注意: 一時的な値下げの場合、t >= 2においてはベースラインのCCPを参照する
scenario_current <-
case_when(scenario %in% c('Baseline', 'Permanent') ~ scenario,
scenario == 'Temporary' ~ 'Baseline')
# 分布の推移
# 注意: 最終期は計算する必要がない
if (t != num_period_sim){
consumer_dist_sim[t+1,] <-
consumer_dist_sim[t,] %*% G_CCP_list[[scenario_current]]
}
# 購買割合の計算
discount_sim_df$prob_buy_sim[t] <-
consumer_dist_sim[t,] %*% CCP_list[[scenario_current]][,2]
# 需要量の計算
discount_sim_df$demand[t] <-
discount_sim_df$prob_buy_sim[t] * num_consumer_sim
# 収入の計算
# 値下げをしている場合のみ、値下げ額を取得
discount <-
case_when(scenario %in% c('Baseline', 'Temporary') ~ 0,
scenario == 'Permanent' ~ 100)
discount_sim_df$revenue[t] <-
state_df %>%
dplyr::mutate(consumer_dist = consumer_dist_sim[t,],
CCP = CCP_list[[scenario_current]][,2],
revenue = (price - discount) * consumer_dist * CCP * num_consumer_sim) %>%
dplyr::summarise(revenue = sum(revenue)) %>%
as.matrix()
}
}
# 購買割合、需要、収入の変数名をシナリオに応じて変更
discount_sim_df <-
discount_sim_df %>%
dplyr::rename(!!str_c('prob_buy_sim_', scenario) := 'prob_buy_sim',
!!str_c('demand_', scenario) := 'demand',
!!str_c('revenue_', scenario) := 'revenue')
}
# ベースラインを基準とした、購買割合の変化を計算
discount_sim_df %>%
dplyr::mutate(PermanentChange = prob_buy_sim_Permanent - prob_buy_sim_Baseline,
TemporaryChange = prob_buy_sim_Temporary - prob_buy_sim_Baseline) %>%
dplyr::select(period, PermanentChange, TemporaryChange) %>%
tidyr::pivot_longer(cols = -period) %>%
dplyr::rename(scenario = name, prob_buy = value) %>%
ggplot2::ggplot(aes(x = period, y = prob_buy, color = scenario)) +
geom_line() +
geom_point() +
ggplot2::theme_minimal() +
# ggplot2::scale_x_continuous(breaks = seq(0, num_period_sim, by = 1)) +
ggplot2::geom_hline(yintercept = 0,
linetype = 'dashed',
color = "blue", size = 0.3)
今回の反実仮想で扱った全てのシナリオ(EDLPと値下げ)の需要と収入を計算する。 そのために、まずEDLPの場合の消費者の分布をシミュレートする。
# データから状態変数毎の消費者の分布を計算。
# mileage_idの順番に並んだ、(1 * 21) の行ベクトル。
consumer_dist_obs_edlp <-
data_gen %>%
dplyr::group_by(mileage_id) %>%
dplyr::summarise(num_obs = n(),
.groups = 'drop') %>%
dplyr::mutate(consumer_dist_obs_edlp = num_obs/sum(num_obs)) %>%
dplyr::arrange(mileage_id) %>%
dplyr::select(consumer_dist_obs_edlp) %>%
as.matrix() %>%
t()
# EDLPのシミュレーション結果を表すデータフレームを前もって作成
edlp_sim_df <-
dplyr::tibble(period = 1:num_period_sim)
# すべての価格についてシミュレーションを行う
for (fixed_price in price_states) {
scenario_edlp <- stringr::str_c('edlp', fixed_price)
# 消費者の分布の推移を表す行列を前もって作成
consumer_dist_sim <- matrix(0, num_period_sim, length(consumer_dist_obs_edlp))
# 初期の分布をデータ上の分布とする
consumer_dist_sim[1,] <- consumer_dist_obs_edlp
# 購買割合、需要、収入の空の列を作成
edlp_sim_df <-
edlp_sim_df %>%
dplyr::mutate(
prob_buy_sim = NA,
demand = NA,
revenue = NA
)
# t期における消費者の分布、購買割合、需要、収入を計算
for (t in 1:num_period_sim) {
# 分布の推移
# 注意: 最終期は計算する必要がない
if (t != num_period_sim){
consumer_dist_sim[t+1,] <-
consumer_dist_sim[t,] %*% G_CCP_list[[scenario_edlp]]
}
# 購買割合の計算
edlp_sim_df$prob_buy_sim[t] <-
consumer_dist_sim[t,] %*% CCP_list[[scenario_edlp]][,2]
# 需要量の計算
edlp_sim_df$demand[t] <-
edlp_sim_df$prob_buy_sim[t] * num_consumer_sim
# 収入の計算
edlp_sim_df$revenue[t] <-
state_df %>%
dplyr::filter(price == fixed_price) %>%
dplyr::arrange(mileage_id) %>%
dplyr::mutate(consumer_dist = consumer_dist_sim[t,],
CCP = CCP_list[[scenario_edlp]][,2],
revenue = fixed_price * consumer_dist * CCP * num_consumer_sim) %>%
dplyr::summarise(revenue = sum(revenue)) %>%
as.matrix()
}
# 購買割合、需要、収入の変数名をシナリオに応じて変更
edlp_sim_df <-
edlp_sim_df %>%
dplyr::rename(!!str_c('prob_buy_sim_', scenario_edlp) := 'prob_buy_sim',
!!str_c('demand_', scenario_edlp) := 'demand',
!!str_c('revenue_', scenario_edlp) := 'revenue')
}
# 結果を図示するためにすべてのシミュレーション結果を統合
sim_df <-
discount_sim_df %>%
dplyr::left_join(edlp_sim_df, by = 'period')
sim_df %>%
dplyr::select(period, stringr::str_c('demand_edlp', price_states)) %>%
tidyr::pivot_longer(cols = - period) %>%
dplyr::rename(scenario = name, demand = value) %>%
ggplot2::ggplot(aes(x = period, y = demand, color = scenario)) +
ggplot2::geom_line() +
ggplot2::geom_point() +
ggplot2::theme_minimal()
sim_df %>%
dplyr::select(period, stringr::str_c('revenue_edlp', price_states)) %>%
tidyr::pivot_longer(cols = - period) %>%
dplyr::rename(scenario = name, revenue = value) %>%
ggplot2::ggplot(aes(x = period, y = revenue, color = scenario)) +
ggplot2::geom_line() +
ggplot2::geom_point() +
ggplot2::theme_minimal()
# 需要の推移を図示
sim_df %>%
dplyr::select(period, stringr::str_c('demand_', discount_scenerio_vec)) %>%
tidyr::pivot_longer(cols = - period) %>%
dplyr::rename(scenario = name, demand = value) %>%
ggplot2::ggplot(aes(x = period, y = demand, color = scenario)) +
ggplot2::geom_line() +
ggplot2::geom_point() +
ggplot2::theme_minimal()
# 需要の累積和を図示
sim_df %>%
dplyr::filter(period <= 20) %>%
dplyr::select(period, stringr::str_c('demand_', discount_scenerio_vec)) %>%
dplyr::mutate(across(starts_with('demand'), cumsum)) %>%
tidyr::pivot_longer(cols = - period) %>%
dplyr::rename(scenario = name, demand = value) %>%
ggplot2::ggplot(aes(x = period, y = demand, color = scenario)) +
ggplot2::geom_line() +
ggplot2::geom_point() +
ggplot2::theme_minimal()
# ベースラインを100とした時の、需要の累積和を図示
sim_df %>%
dplyr::filter(period <= 20) %>%
dplyr::select(period, stringr::str_c('demand_', discount_scenerio_vec)) %>%
dplyr::mutate(across(starts_with('demand'), cumsum)) %>%
dplyr::mutate(Permanent = demand_Permanent/demand_Baseline * 100,
Temporary = demand_Temporary/demand_Baseline * 100) %>%
dplyr::select(period, Permanent, Temporary) %>%
tidyr::pivot_longer(cols = - period) %>%
dplyr::rename(scenario = name, demand = value) %>%
ggplot2::ggplot(aes(x = period, y = demand, color = scenario)) +
ggplot2::geom_line() +
ggplot2::geom_point() +
ggplot2::theme_minimal()
sim_df %>%
dplyr::select(period, stringr::str_c('revenue_', discount_scenerio_vec)) %>%
tidyr::pivot_longer(cols = - period) %>%
dplyr::rename(scenario = name, revenue = value) %>%
ggplot2::ggplot(aes(x = period, y = revenue, color = scenario)) +
ggplot2::geom_line() +
ggplot2::geom_point() +
ggplot2::theme_minimal()
demand_df <-
sim_df %>%
dplyr::summarise(across(starts_with('demand'), sum)) %>%
dplyr::rename_with(~str_replace(., "demand_", "")) %>%
tidyr::pivot_longer(cols = everything()) %>%
dplyr::rename(scenario = name, demand = value)
revenue_df <-
sim_df %>%
# 割引現在価値を計算するため、割引因子の列を追加
dplyr::mutate(beta = beta ^ (period-1)) %>%
dplyr::summarise(across(starts_with('revenue'), ~sum(.x * beta))) %>%
dplyr::rename_with(~str_replace(., "revenue_", "")) %>%
tidyr::pivot_longer(cols = everything()) %>%
dplyr::rename(scenario = name, revenue = value)
demand_df %>%
dplyr::left_join(revenue_df, by = 'scenario')%>%
dplyr::mutate( per_rev = revenue/demand/10) %>%
kableExtra::kable(format = "html",
booktabs = TRUE) %>%
kableExtra::kable_styling()
scenario | demand | revenue | per_rev |
---|---|---|---|
Baseline | 579.1927 | 1190089 | 205.4738 |
Permanent | 620.0550 | 1219207 | 196.6288 |
Temporary | 582.0691 | 1193790 | 205.0941 |
edlp2000 | 709.7349 | 1296483 | 182.6714 |
edlp2100 | 661.4560 | 1267340 | 191.5985 |
edlp2200 | 617.3481 | 1237757 | 200.4958 |
edlp2300 | 576.6884 | 1207384 | 209.3650 |
edlp2400 | 538.9096 | 1175943 | 218.2079 |
edlp2500 | 503.5640 | 1143223 | 227.0263 |