File này phân tích đúng dữ liệu trong dataAGR.xlsx. Nếu bộ dữ liệu hiện tại là dữ liệu mô phỏng/hiệu chỉnh, kết quả phải được trình bày đúng với bản chất mô phỏng/hiệu chỉnh và không được mô tả như dữ liệu quan sát thực tế của Agribank.
Mô hình sử dụng nhận diện đệ quy với ordering:
[ GDPG INF IR NPL. ]
Với (A=I) và (B) tam giác dưới, ma trận tác động cấu trúc (B) chính là Cholesky factor của ma trận hiệp phương sai phần dư VAR. Do đó, các IRF trực giao từ vars::irf(..., ortho = TRUE) và FEVD từ vars::fevd() chính là kết quả của SVAR đệ quy theo ordering trên. Cách triển khai này tránh lỗi bootstrap do update() của đối tượng svarest nhưng không thay đổi chiến lược nhận diện kinh tế lượng.
0. Thiết lập môi trường
Đặt file .qmd này và dataAGR.xlsx trong cùng một thư mục rồi bấm Render .
Xem mã R
if (getRversion () < "4.1.0" ) {
stop ("Cần R >= 4.1.0 để chạy file này." )
}
required_packages <- c (
"readxl" ,
"vars" ,
"urca" ,
"tseries" ,
"strucchange" ,
"ggplot2" ,
"dplyr" ,
"tidyr" ,
"knitr"
)
missing_packages <- required_packages[
! vapply (
required_packages,
requireNamespace,
quietly = TRUE ,
FUN.VALUE = logical (1 )
)
]
Registered S3 method overwritten by 'quantmod':
method from
as.zoo.data.frame zoo
Xem mã R
if (length (missing_packages) > 0 ) {
install.packages (
missing_packages,
repos = "https://cloud.r-project.org"
)
}
still_missing <- required_packages[
! vapply (
required_packages,
requireNamespace,
quietly = TRUE ,
FUN.VALUE = logical (1 )
)
]
if (length (still_missing) > 0 ) {
stop (
paste0 (
"Không cài được package: " ,
paste (still_missing, collapse = ", " ),
". Hãy cài thủ công rồi Render lại."
)
)
}
invisible (
lapply (
required_packages,
library,
character.only = TRUE
)
)
Warning: package 'readxl' was built under R version 4.4.3
Warning: package 'vars' was built under R version 4.4.3
Loading required package: MASS
Loading required package: strucchange
Warning: package 'strucchange' was built under R version 4.4.3
Loading required package: zoo
Warning: package 'zoo' was built under R version 4.4.3
The following objects are masked from 'package:base':
as.Date, as.Date.numeric
Loading required package: sandwich
Warning: package 'sandwich' was built under R version 4.4.3
Loading required package: urca
Warning: package 'urca' was built under R version 4.4.3
Loading required package: lmtest
Warning: package 'lmtest' was built under R version 4.4.3
Warning: package 'tseries' was built under R version 4.4.3
Warning: package 'ggplot2' was built under R version 4.4.3
Warning: package 'dplyr' was built under R version 4.4.3
Attaching package: 'dplyr'
The following object is masked from 'package:MASS':
select
The following objects are masked from 'package:stats':
filter, lag
The following objects are masked from 'package:base':
intersect, setdiff, setequal, union
Warning: package 'tidyr' was built under R version 4.4.3
Warning: package 'knitr' was built under R version 4.4.3
Xem mã R
options (
digits = 7 ,
scipen = 999 ,
knitr.kable.NA = ""
)
set.seed (2026 )
data_file <- "dataAGR.xlsx"
output_dir <- "output_chapter4"
if (! dir.exists (output_dir)) {
dir.create (
output_dir,
recursive = TRUE
)
}
ggplot2:: theme_set (
ggplot2:: theme_minimal (
base_size = 12
) +
ggplot2:: theme (
plot.title = ggplot2:: element_text (
face = "bold" ,
size = 14
),
plot.subtitle = ggplot2:: element_text (
size = 11
),
axis.title = ggplot2:: element_text (
face = "bold"
),
panel.grid.minor = ggplot2:: element_blank (),
legend.position = "bottom" ,
strip.text = ggplot2:: element_text (
face = "bold"
)
)
)
safe_p_text <- function (x) {
if (is.na (x)) {
return (NA_character_ )
}
if (x < 0.01 ) {
return ("< 0.01" )
}
if (x > 0.10 ) {
return ("> 0.10" )
}
sprintf ("%.4f" , x)
}
next_quarters <- function (last_quarter, h) {
y <- as.integer (
substr (
last_quarter,
1 ,
4
)
)
q <- as.integer (
sub (
".*Q" ,
"" ,
last_quarter
)
)
out <- character (h)
for (i in seq_len (h)) {
q <- q + 1
if (q == 5 ) {
q <- 1
y <- y + 1
}
out[i] <- paste0 (
y,
"Q" ,
q
)
}
out
}
quarter_index <- function (x) {
y <- as.integer (
substr (
x,
1 ,
4
)
)
q <- as.integer (
sub (
".*Q" ,
"" ,
x
)
)
4 L * y + q
}
find_htest <- function (object, candidates = character ()) {
if (inherits (object, "htest" )) {
return (object)
}
if (! is.list (object)) {
return (NULL )
}
for (nm in candidates) {
if (
! is.null (object[[nm]]) &&
inherits (
object[[nm]],
"htest"
)
) {
return (
object[[nm]]
)
}
}
idx <- which (
vapply (
object,
inherits,
logical (1 ),
what = "htest"
)
)
if (length (idx) >= 1 ) {
return (
object[[idx[1 ]]]
)
}
NULL
}
htest_row <- function (test_name, htest_object) {
if (
is.null (htest_object) ||
! inherits (
htest_object,
"htest"
)
) {
return (
data.frame (
Test = test_name,
Statistic = NA_real_ ,
df = NA_real_ ,
p_value = NA_real_ ,
stringsAsFactors = FALSE
)
)
}
df_value <- if (
! is.null (
htest_object$ parameter
)
) {
as.numeric (
htest_object$ parameter[1 ]
)
} else {
NA_real_
}
data.frame (
Test = test_name,
Statistic = as.numeric (
htest_object$ statistic[1 ]
),
df = df_value,
p_value = as.numeric (
htest_object$ p.value[1 ]
),
stringsAsFactors = FALSE
)
}
write_csv_utf8 <- function (x, filename) {
utils:: write.csv (
x,
file.path (
output_dir,
filename
),
row.names = FALSE ,
fileEncoding = "UTF-8"
)
}
package_versions <- data.frame (
Package = required_packages,
Version = vapply (
required_packages,
function (p) {
as.character (
utils:: packageVersion (p)
)
},
character (1 )
),
stringsAsFactors = FALSE
)
knitr:: kable (
package_versions,
caption = "Phiên bản các package sử dụng"
)
Phiên bản các package sử dụng
readxl
readxl
1.4.5
vars
vars
1.6.1
urca
urca
1.3.4
tseries
tseries
0.10.58
strucchange
strucchange
1.5.4
ggplot2
ggplot2
4.0.0
dplyr
dplyr
1.1.4
tidyr
tidyr
1.3.1
knitr
knitr
1.50
1. Đọc và kiểm tra dữ liệu
1.1. Đọc file Excel
Xem mã R
if (! file.exists (data_file)) {
stop (
paste0 (
"Không tìm thấy '" ,
data_file,
"'. Hãy đặt file Excel cùng thư mục với file QMD."
)
)
}
available_sheets <- readxl:: excel_sheets (
data_file
)
if (! ("data" %in% available_sheets)) {
stop (
paste0 (
"Không tìm thấy sheet 'data'. Các sheet hiện có: " ,
paste (
available_sheets,
collapse = ", "
)
)
)
}
df <- readxl:: read_excel (
data_file,
sheet = "data"
)
required_columns <- c (
"Quarter" ,
"GDPG" ,
"INF" ,
"IR" ,
"NPL" ,
"EX"
)
missing_columns <- setdiff (
required_columns,
names (df)
)
if (length (missing_columns) > 0 ) {
stop (
paste0 (
"Thiếu cột bắt buộc: " ,
paste (
missing_columns,
collapse = ", "
)
)
)
}
df <- df |>
dplyr:: select (
dplyr:: all_of (
required_columns
)
)
1.2. Kiểm tra chất lượng dữ liệu
Xem mã R
numeric_vars <- c (
"GDPG" ,
"INF" ,
"IR" ,
"NPL" ,
"EX"
)
if (
! all (
grepl (
"^[0-9]{4}Q[1-4]$" ,
df$ Quarter
)
)
) {
stop (
"Quarter phải có định dạng YYYYQ1 đến YYYYQ4, ví dụ 2012Q1."
)
}
quarter_idx <- quarter_index (
df$ Quarter
)
quarter_continuous <- all (
diff (
quarter_idx
) == 1
)
quality_table <- data.frame (
Check = c (
"Số quan sát" ,
"Số cột" ,
"Số giá trị thiếu" ,
"Số kỳ bị trùng" ,
"Các biến định lượng đều là numeric" ,
"Chuỗi quý liên tục"
),
Result = c (
nrow (df),
ncol (df),
sum (is.na (df)),
sum (duplicated (df$ Quarter)),
all (
vapply (
df[numeric_vars],
is.numeric,
logical (1 )
)
),
quarter_continuous
),
stringsAsFactors = FALSE
)
knitr:: kable (
quality_table,
caption = "Kiểm tra chất lượng dữ liệu đầu vào"
)
Kiểm tra chất lượng dữ liệu đầu vào
Số quan sát
56
Số cột
6
Số giá trị thiếu
0
Số kỳ bị trùng
0
Các biến định lượng đều là numeric
1
Chuỗi quý liên tục
1
Xem mã R
if (nrow (df) < 40 ) {
stop (
"Mẫu có dưới 40 quan sát; không tiếp tục pipeline VAR/SVAR hiện tại."
)
}
if (anyNA (df)) {
stop (
"Dữ liệu còn giá trị thiếu."
)
}
if (anyDuplicated (df$ Quarter) > 0 ) {
stop (
"Quarter có kỳ bị trùng."
)
}
if (
! all (
vapply (
df[numeric_vars],
is.numeric,
logical (1 )
)
)
) {
stop (
"Ít nhất một biến GDPG, INF, IR, NPL hoặc EX không phải numeric."
)
}
if (! quarter_continuous) {
stop (
"Chuỗi Quarter không liên tục hoặc đang sai thứ tự."
)
}
cat (
"Dữ liệu hợp lệ:" ,
nrow (df),
"quan sát từ" ,
df$ Quarter[1 ],
"đến" ,
df$ Quarter[nrow (df)],
" \n "
)
Dữ liệu hợp lệ: 56 quan sát từ 2012Q1 đến 2025Q4
2. Tạo chuỗi thời gian và thống kê mô tả
Xem mã R
start_year <- as.integer (
substr (
df$ Quarter[1 ],
1 ,
4
)
)
start_q <- as.integer (
sub (
".*Q" ,
"" ,
df$ Quarter[1 ]
)
)
Y <- stats:: ts (
as.matrix (
df[
,
c (
"GDPG" ,
"INF" ,
"IR" ,
"NPL"
)
]
),
start = c (
start_year,
start_q
),
frequency = 4
)
colnames (Y) <- c (
"GDPG" ,
"INF" ,
"IR" ,
"NPL"
)
stopifnot (
nrow (Y) == nrow (df)
)
2.1. Thống kê mô tả
Xem mã R
desc <- data.frame (
Variable = colnames (Y),
N = apply (
Y,
2 ,
length
),
Mean = apply (
Y,
2 ,
mean
),
SD = apply (
Y,
2 ,
stats:: sd
),
Min = apply (
Y,
2 ,
min
),
Median = apply (
Y,
2 ,
stats:: median
),
Max = apply (
Y,
2 ,
max
),
stringsAsFactors = FALSE
)
desc_display <- desc |>
dplyr:: mutate (
dplyr:: across (
where (is.numeric),
~ round (.x, 4 )
)
)
knitr:: kable (
desc_display,
caption = "Thống kê mô tả các biến baseline"
)
Thống kê mô tả các biến baseline
GDPG
GDPG
56
6.5816
1.4129
3.5114
6.4725
10.5791
INF
INF
56
4.2105
1.0543
1.2957
4.2914
6.3592
IR
IR
56
7.2826
0.6927
5.7397
7.3090
9.0539
NPL
NPL
56
1.9772
0.1652
1.6376
1.9653
2.4158
Xem mã R
write_csv_utf8 (
desc,
"01_descriptive_statistics.csv"
)
2.2. Đồ thị chuỗi thời gian
Xem mã R
plot_df <- df |>
dplyr:: select (
Quarter,
GDPG,
INF,
IR,
NPL
) |>
tidyr:: pivot_longer (
cols = - Quarter,
names_to = "Variable" ,
values_to = "Value"
)
plot_df$ Quarter <- factor (
plot_df$ Quarter,
levels = df$ Quarter
)
quarter_breaks <- df$ Quarter[
seq (
1 ,
nrow (df),
by = 8
)
]
p_ts <- ggplot2:: ggplot (
plot_df,
ggplot2:: aes (
x = Quarter,
y = Value,
group = 1
)
) +
ggplot2:: geom_line (
linewidth = 0.8
) +
ggplot2:: facet_wrap (
~ Variable,
scales = "free_y" ,
ncol = 2
) +
ggplot2:: scale_x_discrete (
breaks = quarter_breaks
) +
ggplot2:: labs (
title = "Diễn biến các biến nghiên cứu theo quý" ,
x = NULL ,
y = "Giá trị (%)"
) +
ggplot2:: theme (
axis.text.x = ggplot2:: element_text (
angle = 45 ,
hjust = 1
)
)
p_ts
Xem mã R
ggplot2:: ggsave (
file.path (
output_dir,
"01_time_series.png"
),
plot = p_ts,
width = 10 ,
height = 7 ,
dpi = 300
)
2.3. Ma trận tương quan
Xem mã R
cor_matrix <- stats:: cor (
as.data.frame (Y),
use = "complete.obs"
)
knitr:: kable (
round (
cor_matrix,
4
),
caption = "Ma trận tương quan giữa các biến baseline"
)
Ma trận tương quan giữa các biến baseline
GDPG
1.0000
0.1361
-0.0583
-0.4673
INF
0.1361
1.0000
0.0565
0.0282
IR
-0.0583
0.0565
1.0000
0.4612
NPL
-0.4673
0.0282
0.4612
1.0000
Xem mã R
utils:: write.csv (
cor_matrix,
file.path (
output_dir,
"02_correlation_matrix.csv"
),
fileEncoding = "UTF-8"
)
3. Kiểm định tính dừng
ADF chính dùng urca::ur.df() với hằng số (drift) và tối đa 4 độ trễ, lựa chọn theo AIC. PP và KPSS được dùng để kiểm tra chéo.
3.1. Hàm ADF
Xem mã R
adf_urca <- function (x, variable_name) {
fit <- urca:: ur.df (
x,
type = "drift" ,
lags = 4 ,
selectlags = "AIC"
)
teststat <- fit@ teststat
cval <- fit@ cval
tau2 <- if (
! is.null (
colnames (teststat)
) &&
"tau2" %in% colnames (teststat)
) {
unname (
teststat[
1 ,
"tau2"
]
)
} else {
unname (
teststat[
1 ,
1
]
)
}
cv1 <- if (
"tau2" %in% rownames (cval)
) {
unname (
cval[
"tau2" ,
"1pct"
]
)
} else {
unname (
cval[
1 ,
"1pct"
]
)
}
cv5 <- if (
"tau2" %in% rownames (cval)
) {
unname (
cval[
"tau2" ,
"5pct"
]
)
} else {
unname (
cval[
1 ,
"5pct"
]
)
}
cv10 <- if (
"tau2" %in% rownames (cval)
) {
unname (
cval[
"tau2" ,
"10pct"
]
)
} else {
unname (
cval[
1 ,
"10pct"
]
)
}
data.frame (
Variable = variable_name,
ADF_tau2 = tau2,
Critical_1pct = cv1,
Critical_5pct = cv5,
Critical_10pct = cv10,
ADF_Conclusion_5pct = ifelse (
tau2 < cv5,
"Dừng I(0)" ,
"Chưa bác bỏ nghiệm đơn vị"
),
stringsAsFactors = FALSE
)
}
3.2. ADF, PP và KPSS
Xem mã R
adf_table <- dplyr:: bind_rows (
lapply (
colnames (Y),
function (v) {
adf_urca (
Y[
,
v
],
v
)
}
)
)
pp_kpss_table <- dplyr:: bind_rows (
lapply (
colnames (Y),
function (v) {
pp_obj <- suppressWarnings (
tseries:: pp.test (
Y[
,
v
]
)
)
kpss_obj <- suppressWarnings (
tseries:: kpss.test (
Y[
,
v
],
null = "Level"
)
)
data.frame (
Variable = v,
PP_statistic = as.numeric (
pp_obj$ statistic[1 ]
),
PP_p_value = as.numeric (
pp_obj$ p.value
),
PP_Conclusion_5pct = ifelse (
pp_obj$ p.value < 0.05 ,
"Dừng" ,
"Chưa dừng"
),
KPSS_statistic = as.numeric (
kpss_obj$ statistic[1 ]
),
KPSS_p_value = as.numeric (
kpss_obj$ p.value
),
KPSS_Conclusion_5pct = ifelse (
kpss_obj$ p.value > 0.05 ,
"Không bác bỏ tính dừng" ,
"Bác bỏ tính dừng"
),
stringsAsFactors = FALSE
)
}
)
)
stationarity_table <- dplyr:: left_join (
adf_table,
pp_kpss_table,
by = "Variable"
)
stationarity_display <- stationarity_table |>
dplyr:: mutate (
dplyr:: across (
c (
ADF_tau2,
Critical_1pct,
Critical_5pct,
Critical_10pct,
PP_statistic,
KPSS_statistic
),
~ round (.x, 4 )
),
PP_p_value = vapply (
PP_p_value,
safe_p_text,
character (1 )
),
KPSS_p_value = vapply (
KPSS_p_value,
safe_p_text,
character (1 )
)
)
knitr:: kable (
stationarity_display,
caption = "Kết quả kiểm định tính dừng"
)
Kết quả kiểm định tính dừng
GDPG
-3.6899
-3.51
-2.89
-2.58
Dừng I(0)
-29.8966
0.0100
Dừng
0.3399
0.1000
Không bác bỏ tính dừng
INF
-4.6760
-3.51
-2.89
-2.58
Dừng I(0)
-31.5940
0.0100
Dừng
0.1985
0.1000
Không bác bỏ tính dừng
IR
-4.1220
-3.51
-2.89
-2.58
Dừng I(0)
-26.8106
0.0100
Dừng
0.1621
0.1000
Không bác bỏ tính dừng
NPL
-4.1929
-3.51
-2.89
-2.58
Dừng I(0)
-24.9760
0.0140
Dừng
0.2795
0.1000
Không bác bỏ tính dừng
Xem mã R
write_csv_utf8 (
stationarity_table,
"03_stationarity_tests.csv"
)
if (
! all (
stationarity_table$ ADF_Conclusion_5pct ==
"Dừng I(0)"
)
) {
warning (
"Có biến chưa đạt ADF 5%; cần đọc đồng thời PP và KPSS trước khi diễn giải."
)
}
4. Lựa chọn độ trễ tối ưu
Xem mã R
lag_selection <- vars:: VARselect (
Y,
lag.max = 4 ,
type = "const"
)
lag_selected <- data.frame (
Criterion = names (
lag_selection$ selection
),
Selected_Lag = as.integer (
lag_selection$ selection
),
stringsAsFactors = FALSE
)
criteria_table <- as.data.frame (
t (
lag_selection$ criteria
)
)
criteria_table$ Lag <- seq_len (
nrow (
criteria_table
)
)
criteria_table <- criteria_table |>
dplyr:: select (
Lag,
dplyr:: everything ()
)
knitr:: kable (
lag_selected,
caption = "Độ trễ tối ưu theo các tiêu chí"
)
Độ trễ tối ưu theo các tiêu chí
AIC(n)
1
HQ(n)
1
SC(n)
1
FPE(n)
1
Xem mã R
knitr:: kable (
criteria_table |>
dplyr:: mutate (
dplyr:: across (
where (is.numeric),
~ round (.x, 6 )
)
),
caption = "Giá trị các tiêu chí lựa chọn độ trễ"
)
Giá trị các tiêu chí lựa chọn độ trễ
1
-5.176127
-4.888412
-4.425649
0.005663
2
-4.792810
-4.274921
-3.441949
0.008407
3
-4.645658
-3.897597
-2.694415
0.010028
4
-4.604138
-3.625905
-2.052512
0.011059
Xem mã R
write_csv_utf8 (
lag_selected,
"04_lag_selection.csv"
)
if (
length (
unique (
lag_selected$ Selected_Lag
)
) == 1
) {
p_opt <- unique (
lag_selected$ Selected_Lag
)
} else if (
"SC(n)" %in%
lag_selected$ Criterion
) {
p_opt <- lag_selected$ Selected_Lag[
lag_selected$ Criterion ==
"SC(n)"
][1 ]
} else {
p_opt <- 1 L
}
p_opt <- as.integer (
p_opt
)
if (
is.na (p_opt) ||
p_opt < 1
) {
p_opt <- 1 L
}
cat (
"Độ trễ baseline được sử dụng:" ,
p_opt,
" \n "
)
Độ trễ baseline được sử dụng: 1
5. Ước lượng VAR và chẩn đoán
5.1. Ước lượng VAR
Xem mã R
var_model <- vars:: VAR (
Y,
p = p_opt,
type = "const"
)
if (
! inherits (
var_model,
"varest"
)
) {
stop (
"Không tạo được đối tượng VAR lớp 'varest'."
)
}
var_equations <- var_model$ varresult
if (
is.null (var_equations) ||
length (var_equations) != ncol (Y) ||
! all (
vapply (
var_equations,
inherits,
logical (1 ),
what = "lm"
)
)
) {
stop (
"Không thể trích xuất đầy đủ các phương trình lm từ VAR."
)
}
coef_table <- dplyr:: bind_rows (
lapply (
names (var_equations),
function (eq) {
cm <- summary (
var_equations[[eq]]
)$ coefficients
data.frame (
Equation = eq,
Term = rownames (cm),
Estimate = as.numeric (
cm[
,
1
]
),
Std_Error = as.numeric (
cm[
,
2
]
),
t_value = as.numeric (
cm[
,
3
]
),
p_value = as.numeric (
cm[
,
4
]
),
row.names = NULL ,
stringsAsFactors = FALSE
)
}
)
)
knitr:: kable (
coef_table |>
dplyr:: mutate (
dplyr:: across (
c (
Estimate,
Std_Error,
t_value,
p_value
),
~ round (.x, 5 )
)
),
caption = "Hệ số ước lượng VAR"
)
Hệ số ước lượng VAR
GDPG
GDPG.l1
0.52834
0.13769
3.83721
0.00035
GDPG
INF.l1
0.10587
0.15949
0.66378
0.50988
GDPG
IR.l1
-0.72566
0.27512
-2.63763
0.01110
GDPG
NPL.l1
1.73298
1.31106
1.32181
0.19225
GDPG
const
4.50522
2.83726
1.58788
0.11862
INF
GDPG.l1
-0.02105
0.10744
-0.19593
0.84546
INF
INF.l1
0.44220
0.12446
3.55299
0.00084
INF
IR.l1
-0.34878
0.21469
-1.62459
0.11054
INF
NPL.l1
-0.42035
1.02307
-0.41087
0.68293
INF
const
5.85917
2.21402
2.64639
0.01085
IR
GDPG.l1
-0.05132
0.06475
-0.79250
0.43181
IR
INF.l1
0.17124
0.07501
2.28299
0.02672
IR
IR.l1
0.49446
0.12938
3.82174
0.00037
IR
NPL.l1
0.21752
0.61656
0.35279
0.72573
IR
const
2.86924
1.33428
2.15040
0.03638
NPL
GDPG.l1
-0.06912
0.01151
-6.00581
0.00000
NPL
INF.l1
0.02713
0.01333
2.03496
0.04717
NPL
IR.l1
0.06816
0.02300
2.96384
0.00464
NPL
NPL.l1
0.20196
0.10959
1.84281
0.07129
NPL
const
1.42478
0.23717
6.00741
0.00000
Xem mã R
write_csv_utf8 (
coef_table,
"05_VAR_coefficients.csv"
)
5.2. Tính ổn định của VAR
Xem mã R
roots_var <- vars:: roots (
var_model,
modulus = TRUE
)
roots_table <- data.frame (
Root = seq_along (
roots_var
),
Modulus = as.numeric (
roots_var
),
Stable = as.numeric (
roots_var
) < 1 ,
stringsAsFactors = FALSE
)
knitr:: kable (
roots_table |>
dplyr:: mutate (
Modulus = round (
Modulus,
6
)
),
caption = "Modulus của các nghiệm đặc trưng"
)
Modulus của các nghiệm đặc trưng
1
0.580142
TRUE
2
0.580142
TRUE
3
0.428530
TRUE
4
0.428530
TRUE
Xem mã R
var_stable <- all (
roots_var < 1
)
if (! var_stable) {
stop (
"VAR không ổn định vì có modulus >= 1."
)
}
p_roots <- ggplot2:: ggplot (
roots_table,
ggplot2:: aes (
x = factor (Root),
y = Modulus
)
) +
ggplot2:: geom_col (
width = 0.65
) +
ggplot2:: geom_hline (
yintercept = 1 ,
linetype = "dashed" ,
linewidth = 0.7
) +
ggplot2:: labs (
title = "Độ lớn các nghiệm đặc trưng của VAR" ,
subtitle = "Điều kiện ổn định: tất cả modulus < 1" ,
x = "Nghiệm" ,
y = "Modulus"
)
p_roots
Xem mã R
ggplot2:: ggsave (
file.path (
output_dir,
"05_VAR_roots.png"
),
plot = p_roots,
width = 8 ,
height = 4.5 ,
dpi = 300
)
5.3. Tự tương quan, ARCH và phân phối chuẩn
Xem mã R
serial_test <- vars:: serial.test (
var_model,
lags.pt = 8 ,
type = "PT.adjusted"
)
arch_test <- vars:: arch.test (
var_model,
lags.multi = 4 ,
multivariate.only = TRUE
)
normality_test <- vars:: normality.test (
var_model,
multivariate.only = TRUE
)
# vars 1.6-1 trả Portmanteau ở $serial.
# Hàm find_htest vẫn có fallback để tránh phụ thuộc cứng vào tên phần tử.
serial_ht <- find_htest (
serial_test,
candidates = c (
"serial" ,
"pt.mul"
)
)
arch_ht <- find_htest (
arch_test,
candidates = c (
"arch.mul"
)
)
jb_container <- normality_test$ jb.mul
if (is.null (jb_container)) {
jb_container <- normality_test$ jb.multi
}
jb_ht <- if (
! is.null (jb_container)
) {
find_htest (
jb_container,
candidates = c (
"JB"
)
)
} else {
NULL
}
skew_ht <- if (
! is.null (jb_container)
) {
find_htest (
jb_container,
candidates = c (
"Skewness"
)
)
} else {
NULL
}
kurt_ht <- if (
! is.null (jb_container)
) {
find_htest (
jb_container,
candidates = c (
"Kurtosis"
)
)
} else {
NULL
}
if (is.null (serial_ht)) {
stop (
"Không đọc được kết quả Portmanteau từ serial.test()."
)
}
if (is.null (arch_ht)) {
stop (
"Không đọc được kết quả ARCH đa biến."
)
}
if (
is.null (jb_ht) ||
is.null (skew_ht) ||
is.null (kurt_ht)
) {
stop (
"Không đọc được đầy đủ kết quả normality.test()."
)
}
diagnostic_table <- dplyr:: bind_rows (
htest_row (
"Portmanteau điều chỉnh" ,
serial_ht
),
htest_row (
"ARCH đa biến" ,
arch_ht
),
htest_row (
"Jarque-Bera đa biến" ,
jb_ht
),
htest_row (
"Skewness đa biến" ,
skew_ht
),
htest_row (
"Kurtosis đa biến" ,
kurt_ht
)
)
diagnostic_table$ Status_5pct <- ifelse (
diagnostic_table$ p_value > 0.05 ,
"Đạt" ,
"Cần xem xét"
)
knitr:: kable (
diagnostic_table |>
dplyr:: mutate (
Statistic = round (
Statistic,
4
),
df = round (
df,
0
),
p_value = round (
p_value,
4
)
),
caption = "Các kiểm định chẩn đoán VAR"
)
Các kiểm định chẩn đoán VAR
Portmanteau điều chỉnh
103.2390
112
0.7111
Đạt
ARCH đa biến
391.1562
400
0.6146
Đạt
Jarque-Bera đa biến
11.8767
8
0.1568
Đạt
Skewness đa biến
5.6023
4
0.2309
Đạt
Kurtosis đa biến
6.2744
4
0.1796
Đạt
Xem mã R
write_csv_utf8 (
diagnostic_table,
"06_VAR_diagnostics.csv"
)
5.4. OLS-CUSUM
Xem mã R
stability_test <- vars:: stability (
var_model,
type = "OLS-CUSUM"
)
if (
is.null (
stability_test$ stability
) ||
length (
stability_test$ stability
) != ncol (Y)
) {
stop (
"Không tạo được đầy đủ OLS-CUSUM cho bốn phương trình."
)
}
cusum_names <- names (
stability_test$ stability
)
if (
is.null (cusum_names) ||
length (cusum_names) != ncol (Y)
) {
cusum_names <- colnames (Y)
}
cusum_test_table <- dplyr:: bind_rows (
lapply (
seq_along (
stability_test$ stability
),
function (i) {
tst <- strucchange:: sctest (
stability_test$ stability[[i]]
)
data.frame (
Equation = cusum_names[i],
Statistic = as.numeric (
tst$ statistic[1 ]
),
p_value = as.numeric (
tst$ p.value
),
Status_5pct = ifelse (
tst$ p.value > 0.05 ,
"Đạt" ,
"Cần xem xét"
),
stringsAsFactors = FALSE
)
}
)
)
knitr:: kable (
cusum_test_table |>
dplyr:: mutate (
Statistic = round (
Statistic,
4
),
p_value = round (
p_value,
4
)
),
caption = "Kiểm định OLS-CUSUM theo từng phương trình"
)
Kiểm định OLS-CUSUM theo từng phương trình
S0…1
GDPG
1.1253
0.1588
Đạt
S0…2
INF
0.6884
0.7305
Đạt
S0…3
IR
0.7874
0.5647
Đạt
S0…4
NPL
0.8570
0.4548
Đạt
Xem mã R
write_csv_utf8 (
cusum_test_table,
"07_CUSUM_tests.csv"
)
plot (
stability_test,
plot.type = "multiple" ,
nc = 2
)
Xem mã R
grDevices:: png (
file.path (
output_dir,
"07_CUSUM.png"
),
width = 1800 ,
height = 1400 ,
res = 200
)
plot (
stability_test,
plot.type = "multiple" ,
nc = 2
)
grDevices:: dev.off ()
6. Nhận diện SVAR đệ quy
Với (A=I) và (B) tam giác dưới:
[ u_t = B_t. ]
Trong nhận diện đệ quy, (B) là Cholesky factor của covariance phần dư VAR theo ordering GDPG → INF → IR → NPL.
6.1. Ma trận ràng buộc và ma trận B
Xem mã R
K <- ncol (Y)
B_restriction <- matrix (
0 ,
nrow = K,
ncol = K,
dimnames = list (
colnames (Y),
colnames (Y)
)
)
B_restriction[
lower.tri (
B_restriction,
diag = TRUE
)
] <- NA
residual_df <- as.numeric (
summary (
var_model$ varresult[[1 ]]
)$ df[2 ]
)
if (
! is.finite (residual_df) ||
residual_df <= 0
) {
stop (
"Không xác định được residual degrees of freedom của VAR."
)
}
U <- residuals (
var_model
)
Sigma_u <- crossprod (
U
) / residual_df
B_recursive <- t (
chol (
Sigma_u
)
)
dimnames (
B_recursive
) <- list (
colnames (Y),
colnames (Y)
)
B_table <- data.frame (
Response = rownames (
B_recursive
),
B_recursive,
check.names = FALSE ,
stringsAsFactors = FALSE
)
B_display <- B_table |>
dplyr:: mutate (
dplyr:: across (
- Response,
~ round (.x, 6 )
)
)
knitr:: kable (
B_restriction,
caption = "Ma trận ràng buộc B của SVAR đệ quy"
)
Ma trận ràng buộc B của SVAR đệ quy
GDPG
0
0
0
INF
0
0
IR
0
NPL
Xem mã R
knitr:: kable (
B_display,
caption = "Ma trận tác động cấu trúc B theo nhận diện đệ quy"
)
Ma trận tác động cấu trúc B theo nhận diện đệ quy
GDPG
GDPG
1.228395
0.000000
0.000000
0.000000
INF
INF
0.013826
0.958466
0.000000
0.000000
IR
IR
0.091964
0.063082
0.566813
0.000000
NPL
NPL
-0.021077
0.019021
0.029158
0.094274
Xem mã R
write_csv_utf8 (
B_table,
"08_recursive_SVAR_B_matrix.csv"
)
utils:: write.csv (
Sigma_u,
file.path (
output_dir,
"08_VAR_residual_covariance.csv"
),
fileEncoding = "UTF-8"
)
6.2. Kiểm tra đối chiếu bằng SVAR()
Phần này chỉ dùng để xác nhận rằng ước lượng SVAR() cho cùng ma trận B. Toàn bộ IRF/FEVD/Stress Testing phía sau không phụ thuộc vào object svarest, nên nếu một phiên bản vars có vấn đề tối ưu hóa thì pipeline chính vẫn tiếp tục.
Xem mã R
svar_check <- tryCatch (
{
vars:: SVAR (
x = var_model,
estmethod = "scoring" ,
Bmat = B_restriction,
max.iter = 1000 ,
maxls = 1000 ,
conv.crit = 1e-8 ,
lrtest = FALSE
)
},
error = function (e) {
structure (
list (
error = conditionMessage (e)
),
class = "svar_failed"
)
}
)
svar_check_ok <- inherits (
svar_check,
"svarest"
)
if (svar_check_ok) {
max_B_difference <- max (
abs (
svar_check$ B -
B_recursive
)
)
svar_crosscheck_table <- data.frame (
Metric = c (
"SVAR chạy thành công" ,
"Số vòng lặp" ,
"Sai khác lớn nhất giữa B-SVAR và B-Cholesky"
),
Value = c (
"TRUE" ,
as.character (
svar_check$ iter
),
format (
max_B_difference,
scientific = TRUE ,
digits = 4
)
),
stringsAsFactors = FALSE
)
Bse_table <- data.frame (
Response = rownames (
svar_check$ Bse
),
svar_check$ Bse,
check.names = FALSE ,
stringsAsFactors = FALSE
)
knitr:: kable (
Bse_table |>
dplyr:: mutate (
dplyr:: across (
- Response,
~ round (.x, 6 )
)
),
caption = "Sai số chuẩn của ma trận B từ SVAR scoring"
)
write_csv_utf8 (
Bse_table,
"08_SVAR_B_standard_errors.csv"
)
} else {
max_B_difference <- NA_real_
svar_crosscheck_table <- data.frame (
Metric = c (
"SVAR chạy thành công" ,
"Thông báo"
),
Value = c (
"FALSE" ,
svar_check$ error
),
stringsAsFactors = FALSE
)
}
knitr:: kable (
svar_crosscheck_table,
caption = "Đối chiếu nhận diện đệ quy với hàm SVAR()"
)
Đối chiếu nhận diện đệ quy với hàm SVAR()
SVAR chạy thành công
TRUE
Số vòng lặp
12
Sai khác lớn nhất giữa B-SVAR và B-Cholesky
3.4e-15
Xem mã R
write_csv_utf8 (
svar_crosscheck_table,
"08_SVAR_crosscheck.csv"
)
7. Structural IRF
Vì nhận diện là đệ quy, vars::irf() trên var_model với ortho = TRUE sử dụng chính Cholesky factor (B). Đây là IRF cấu trúc của recursive SVAR theo ordering đã xác định.
7.1. Bootstrap IRF
Xem mã R
H <- 8
irf_all <- vars:: irf (
var_model,
impulse = c (
"GDPG" ,
"INF" ,
"IR"
),
response = "NPL" ,
n.ahead = H,
ortho = TRUE ,
cumulative = FALSE ,
boot = TRUE ,
runs = 2000 ,
ci = 0.95 ,
seed = 2026
)
required_impulses <- c (
"GDPG" ,
"INF" ,
"IR"
)
if (
is.null (
irf_all$ irf
) ||
is.null (
irf_all$ Lower
) ||
is.null (
irf_all$ Upper
) ||
! all (
required_impulses %in%
names (
irf_all$ irf
)
) ||
! all (
required_impulses %in%
names (
irf_all$ Lower
)
) ||
! all (
required_impulses %in%
names (
irf_all$ Upper
)
)
) {
stop (
"Bootstrap IRF không trả về đầy đủ point estimate và CI."
)
}
7.2. Bảng IRF
Xem mã R
extract_irf <- function (
irf_object,
impulse_name,
shock_label
) {
point_matrix <- irf_object$ irf[[impulse_name]]
lower_matrix <- irf_object$ Lower[[impulse_name]]
upper_matrix <- irf_object$ Upper[[impulse_name]]
point <- as.numeric (
point_matrix[
,
"NPL"
]
)
lower <- as.numeric (
lower_matrix[
,
"NPL"
]
)
upper <- as.numeric (
upper_matrix[
,
"NPL"
]
)
if (
length (point) != H + 1 ||
length (lower) != H + 1 ||
length (upper) != H + 1
) {
stop (
paste0 (
"IRF của " ,
impulse_name,
" không có đúng H+1 quan sát."
)
)
}
data.frame (
Shock = shock_label,
Horizon = 0 : H,
IRF = point,
Lower_95 = lower,
Upper_95 = upper,
Significant_5pct = ifelse (
lower > 0 |
upper < 0 ,
"Có" ,
"Không"
),
stringsAsFactors = FALSE
)
}
irf_table <- dplyr:: bind_rows (
extract_irf (
irf_all,
"GDPG" ,
"Cú sốc tăng trưởng GDP"
),
extract_irf (
irf_all,
"INF" ,
"Cú sốc lạm phát"
),
extract_irf (
irf_all,
"IR" ,
"Cú sốc lãi suất"
)
)
knitr:: kable (
irf_table |>
dplyr:: mutate (
dplyr:: across (
c (
IRF,
Lower_95,
Upper_95
),
~ round (.x, 5 )
)
),
caption = "Phản ứng của NPL trước các cú sốc cấu trúc"
)
Phản ứng của NPL trước các cú sốc cấu trúc
Cú sốc tăng trưởng GDP
0
-0.02108
-0.04515
0.00581
Không
Cú sốc tăng trưởng GDP
1
-0.08253
-0.10889
-0.04930
Có
Cú sốc tăng trưởng GDP
2
-0.05701
-0.08168
-0.01563
Có
Cú sốc tăng trưởng GDP
3
-0.02630
-0.05188
0.00968
Không
Cú sốc tăng trưởng GDP
4
-0.00952
-0.03152
0.01704
Không
Cú sốc tăng trưởng GDP
5
-0.00303
-0.02084
0.01553
Không
Cú sốc tăng trưởng GDP
6
-0.00095
-0.01501
0.01179
Không
Cú sốc tăng trưởng GDP
7
-0.00023
-0.01061
0.00865
Không
Cú sốc tăng trưởng GDP
8
0.00010
-0.00765
0.00562
Không
Cú sốc lạm phát
0
0.01902
-0.00460
0.03971
Không
Cú sốc lạm phát
1
0.03414
0.00150
0.05808
Có
Cú sốc lạm phát
2
0.02505
-0.01260
0.05393
Không
Cú sốc lạm phát
3
0.01877
-0.01513
0.04797
Không
Cú sốc lạm phát
4
0.01477
-0.01079
0.03919
Không
Cú sốc lạm phát
5
0.01081
-0.00606
0.02790
Không
Cú sốc lạm phát
6
0.00688
-0.00458
0.01936
Không
Cú sốc lạm phát
7
0.00371
-0.00504
0.01273
Không
Cú sốc lạm phát
8
0.00162
-0.00506
0.00842
Không
Cú sốc lãi suất
0
0.02916
0.00508
0.05236
Có
Cú sốc lãi suất
1
0.04452
0.01657
0.06378
Có
Cú sốc lãi suất
2
0.04777
0.01153
0.07101
Có
Cú sốc lãi suất
3
0.03700
0.00056
0.06063
Có
Cú sốc lãi suất
4
0.02250
-0.00631
0.04404
Không
Cú sốc lãi suất
5
0.01104
-0.01055
0.02893
Không
Cú sốc lãi suất
6
0.00417
-0.01120
0.01942
Không
Cú sốc lãi suất
7
0.00085
-0.00991
0.01295
Không
Cú sốc lãi suất
8
-0.00042
-0.00807
0.00934
Không
Xem mã R
write_csv_utf8 (
irf_table,
"09_IRF_NPL.csv"
)
7.3. Kiểm tra IRF với Psi()
Xem mã R
Psi_baseline <- vars:: Psi (
var_model,
nstep = H
)
dimnames (Psi_baseline) <- list (
colnames (Y),
colnames (Y),
NULL
)
manual_irf <- dplyr:: bind_rows (
lapply (
required_impulses,
function (imp) {
data.frame (
Shock = imp,
Horizon = 0 : H,
Psi_IRF = as.numeric (
Psi_baseline[
"NPL" ,
imp,
1 : (H + 1 )
]
),
Package_IRF = as.numeric (
irf_all$ irf[[imp]][
,
"NPL"
]
),
stringsAsFactors = FALSE
)
}
)
)
max_irf_difference <- max (
abs (
manual_irf$ Psi_IRF -
manual_irf$ Package_IRF
)
)
irf_sanity <- data.frame (
Check = "max |Psi IRF - irf() point estimate|" ,
Value = max_irf_difference,
Pass = max_irf_difference < 1e-8 ,
stringsAsFactors = FALSE
)
knitr:: kable (
irf_sanity |>
dplyr:: mutate (
Value = format (
Value,
scientific = TRUE ,
digits = 4
)
),
caption = "Kiểm tra nhất quán của IRF"
)
Kiểm tra nhất quán của IRF
max |Psi IRF - irf() point estimate|
0e+00
TRUE
Xem mã R
if (
! isTRUE (
irf_sanity$ Pass[1 ]
)
) {
stop (
"Point IRF từ irf() không khớp Psi(); dừng để tránh diễn giải sai."
)
}
7.4. Đồ thị IRF
Xem mã R
p_irf <- ggplot2:: ggplot (
irf_table,
ggplot2:: aes (
x = Horizon,
y = IRF
)
) +
ggplot2:: geom_hline (
yintercept = 0 ,
linetype = "dashed" ,
linewidth = 0.45
) +
ggplot2:: geom_ribbon (
ggplot2:: aes (
ymin = Lower_95,
ymax = Upper_95
),
alpha = 0.18
) +
ggplot2:: geom_line (
linewidth = 0.9
) +
ggplot2:: geom_point (
size = 1.8
) +
ggplot2:: facet_wrap (
~ Shock,
ncol = 1 ,
scales = "free_y"
) +
ggplot2:: scale_x_continuous (
breaks = 0 : H
) +
ggplot2:: labs (
title = "Hàm phản ứng xung cấu trúc của NPL" ,
subtitle = "Recursive SVAR; bootstrap 2.000 lần; khoảng tin cậy 95%" ,
x = "Số quý sau cú sốc" ,
y = "Phản ứng của NPL (điểm phần trăm)"
)
p_irf
Xem mã R
ggplot2:: ggsave (
file.path (
output_dir,
"09_IRF_NPL.png"
),
plot = p_irf,
width = 10 ,
height = 7 ,
dpi = 300
)
7.5. Peak response
Xem mã R
peak_irf <- irf_table |>
dplyr:: group_by (
Shock
) |>
dplyr:: slice_max (
order_by = abs (IRF),
n = 1 ,
with_ties = FALSE
) |>
dplyr:: ungroup ()
knitr:: kable (
peak_irf |>
dplyr:: mutate (
dplyr:: across (
c (
IRF,
Lower_95,
Upper_95
),
~ round (.x, 5 )
)
),
caption = "Phản ứng cực đại của NPL"
)
Phản ứng cực đại của NPL
Cú sốc lãi suất
2
0.04777
0.01153
0.07101
Có
Cú sốc lạm phát
1
0.03414
0.00150
0.05808
Có
Cú sốc tăng trưởng GDP
1
-0.08253
-0.10889
-0.04930
Có
Xem mã R
write_csv_utf8 (
peak_irf,
"10_IRF_peak_response.csv"
)
8. Forecast Error Variance Decomposition
vars::fevd(var_model) sử dụng các hệ số MA trực giao từ Psi(), do đó tương ứng với cùng recursive SVAR.
Xem mã R
fevd_result <- vars:: fevd (
var_model,
n.ahead = H
)
if (
is.null (
fevd_result$ NPL
)
) {
stop (
"FEVD không trả về thành phần NPL."
)
}
fevd_npl <- as.matrix (
fevd_result$ NPL
)
if (
nrow (fevd_npl) != H
) {
stop (
"FEVD NPL không có đúng số horizon yêu cầu."
)
}
fevd_npl_pct <- 100 *
fevd_npl
row_sum_error <- max (
abs (
rowSums (
fevd_npl
) -
1
)
)
if (
row_sum_error > 1e-6
) {
stop (
"Tổng tỷ trọng FEVD khác 1 vượt quá sai số số học cho phép."
)
}
fevd_table <- data.frame (
Horizon = 1 : H,
fevd_npl_pct,
check.names = FALSE
)
knitr:: kable (
fevd_table |>
dplyr:: mutate (
dplyr:: across (
where (is.numeric),
~ round (.x, 2 )
)
),
caption = "Phân rã phương sai sai số dự báo của NPL (%)"
)
Phân rã phương sai sai số dự báo của NPL (%)
1
4.21
3.43
8.06
84.29
2
34.77
7.32
13.58
44.33
3
38.80
7.96
18.89
34.35
4
37.85
8.48
21.92
31.75
5
37.10
8.96
22.97
30.97
6
36.82
9.26
23.18
30.74
7
36.74
9.40
23.19
30.68
8
36.72
9.44
23.18
30.66
Xem mã R
write_csv_utf8 (
fevd_table,
"11_FEVD_NPL.csv"
)
fevd_selected <- fevd_table |>
dplyr:: filter (
Horizon %in% c (
1 ,
4 ,
8
)
)
knitr:: kable (
fevd_selected |>
dplyr:: mutate (
dplyr:: across (
where (is.numeric),
~ round (.x, 2 )
)
),
caption = "FEVD của NPL tại quý 1, 4 và 8"
)
FEVD của NPL tại quý 1, 4 và 8
1
4.21
3.43
8.06
84.29
4
37.85
8.48
21.92
31.75
8
36.72
9.44
23.18
30.66
Xem mã R
write_csv_utf8 (
fevd_selected,
"11_FEVD_NPL_Q1_Q4_Q8.csv"
)
8.1. Đồ thị FEVD
Xem mã R
fevd_long <- fevd_table |>
tidyr:: pivot_longer (
cols = - Horizon,
names_to = "Shock" ,
values_to = "Contribution"
)
p_fevd <- ggplot2:: ggplot (
fevd_long,
ggplot2:: aes (
x = Horizon,
y = Contribution,
fill = Shock
)
) +
ggplot2:: geom_area (
position = "stack" ,
alpha = 0.85
) +
ggplot2:: scale_x_continuous (
breaks = 1 : H
) +
ggplot2:: coord_cartesian (
ylim = c (
0 ,
100
)
) +
ggplot2:: labs (
title = "Phân rã phương sai sai số dự báo của NPL" ,
subtitle = "Tỷ trọng đóng góp của các cú sốc cấu trúc" ,
x = "Chân trời dự báo (quý)" ,
y = "Tỷ trọng (%)" ,
fill = "Cú sốc"
)
p_fevd
Xem mã R
ggplot2:: ggsave (
file.path (
output_dir,
"11_FEVD_NPL.png"
),
plot = p_fevd,
width = 10 ,
height = 6 ,
dpi = 300
)
8.2. Tổng đóng góp cú sốc vĩ mô
Xem mã R
needed_fevd_columns <- c (
"GDPG" ,
"INF" ,
"IR" ,
"NPL"
)
if (
! all (
needed_fevd_columns %in%
names (
fevd_table
)
)
) {
stop (
"FEVD thiếu ít nhất một cột GDPG, INF, IR hoặc NPL."
)
}
fevd_macro <- data.frame (
Horizon = fevd_table$ Horizon,
Macro_Total =
fevd_table$ GDPG +
fevd_table$ INF +
fevd_table$ IR,
Own_NPL_Shock =
fevd_table$ NPL
)
knitr:: kable (
fevd_macro |>
dplyr:: mutate (
dplyr:: across (
where (is.numeric),
~ round (.x, 2 )
)
),
caption = "Đóng góp tổng hợp của các cú sốc vĩ mô vào NPL"
)
Đóng góp tổng hợp của các cú sốc vĩ mô vào NPL
1
15.71
84.29
2
55.67
44.33
3
65.65
34.35
4
68.25
31.75
5
69.03
30.97
6
69.26
30.74
7
69.32
30.68
8
69.34
30.66
Xem mã R
write_csv_utf8 (
fevd_macro,
"12_FEVD_macro_total.csv"
)
9. Robustness
Các mô hình robustness sử dụng cùng nguyên tắc Cholesky/recursive identification. Đây là kiểm tra độ nhạy của IRF trước thay đổi lag, ordering và tập biến.
9.1. Hàm IRF point estimate cho robustness
Xem mã R
extract_point_irf <- function (
varest_model,
impulse_name,
shock_label,
specification
) {
obj <- vars:: irf (
varest_model,
impulse = impulse_name,
response = "NPL" ,
n.ahead = H,
ortho = TRUE ,
cumulative = FALSE ,
boot = FALSE
)
values <- as.numeric (
obj$ irf[[impulse_name]][
,
"NPL"
]
)
data.frame (
Shock = shock_label,
Horizon = 0 : H,
IRF = values,
Specification = specification,
stringsAsFactors = FALSE
)
}
serial_p_value <- function (
varest_model
) {
out <- tryCatch (
vars:: serial.test (
varest_model,
lags.pt = 8 ,
type = "PT.adjusted"
),
error = function (e) {
NULL
}
)
if (is.null (out)) {
return (
NA_real_
)
}
ht <- find_htest (
out,
candidates = c (
"serial" ,
"pt.mul"
)
)
if (is.null (ht)) {
return (
NA_real_
)
}
as.numeric (
ht$ p.value
)
}
9.2. VAR(2)
Xem mã R
robust_lag2_ok <- FALSE
var_p2 <- NULL
max_root_p2 <- NA_real_
serial_p2 <- NA_real_
lag2_message <- ""
tryCatch (
{
var_p2 <- vars:: VAR (
Y,
p = 2 ,
type = "const"
)
max_root_p2 <- max (
vars:: roots (
var_p2,
modulus = TRUE
)
)
serial_p2 <- serial_p_value (
var_p2
)
robust_lag2_ok <- is.finite (
max_root_p2
) &&
max_root_p2 < 1
if (! robust_lag2_ok) {
lag2_message <- "VAR(2) không ổn định."
}
},
error = function (e) {
robust_lag2_ok <<- FALSE
lag2_message <<- conditionMessage (e)
}
)
9.3. Alternative ordering
[ GDPG IR INF NPL. ]
Xem mã R
robust_order_ok <- FALSE
var_alt <- NULL
max_root_alt <- NA_real_
serial_alt <- NA_real_
order_message <- ""
tryCatch (
{
Y_alt <- stats:: ts (
as.matrix (
df[
,
c (
"GDPG" ,
"IR" ,
"INF" ,
"NPL"
)
]
),
start = c (
start_year,
start_q
),
frequency = 4
)
colnames (
Y_alt
) <- c (
"GDPG" ,
"IR" ,
"INF" ,
"NPL"
)
var_alt <- vars:: VAR (
Y_alt,
p = p_opt,
type = "const"
)
max_root_alt <- max (
vars:: roots (
var_alt,
modulus = TRUE
)
)
serial_alt <- serial_p_value (
var_alt
)
robust_order_ok <- is.finite (
max_root_alt
) &&
max_root_alt < 1
if (! robust_order_ok) {
order_message <- "Alternative ordering tạo VAR không ổn định."
}
},
error = function (e) {
robust_order_ok <<- FALSE
order_message <<- conditionMessage (e)
}
)
9.4. Mô hình 5 biến có EX
Xem mã R
EX_ts <- stats:: ts (
df$ EX,
start = c (
start_year,
start_q
),
frequency = 4
)
ex_adf <- adf_urca (
EX_ts,
"EX"
)
ex_pp <- suppressWarnings (
tseries:: pp.test (
EX_ts
)
)
ex_kpss <- suppressWarnings (
tseries:: kpss.test (
EX_ts,
null = "Level"
)
)
ex_stationarity <- data.frame (
Variable = "EX" ,
ADF_tau2 = ex_adf$ ADF_tau2,
ADF_CV_5pct = ex_adf$ Critical_5pct,
ADF_Conclusion = ex_adf$ ADF_Conclusion_5pct,
PP_p_value = as.numeric (
ex_pp$ p.value
),
KPSS_p_value = as.numeric (
ex_kpss$ p.value
),
stringsAsFactors = FALSE
)
knitr:: kable (
ex_stationarity |>
dplyr:: mutate (
dplyr:: across (
c (
ADF_tau2,
ADF_CV_5pct,
PP_p_value,
KPSS_p_value
),
~ round (.x, 5 )
)
),
caption = "Kiểm định tính dừng của EX"
)
Kiểm định tính dừng của EX
EX
-2.09768
-2.89
Chưa bác bỏ nghiệm đơn vị
0.01089
0.1
Xem mã R
robust_ex_ok <- FALSE
var5 <- NULL
ex_message <- ""
p5 <- NA_integer_
max_root_5 <- NA_real_
serial_5 <- NA_real_
tryCatch (
{
if (
ex_adf$ ADF_Conclusion_5pct !=
"Dừng I(0)"
) {
stop (
"EX chưa đạt ADF 5%; không chạy mô hình 5 biến ở mức."
)
}
Y5 <- stats:: ts (
as.matrix (
df[
,
c (
"GDPG" ,
"INF" ,
"IR" ,
"EX" ,
"NPL"
)
]
),
start = c (
start_year,
start_q
),
frequency = 4
)
colnames (
Y5
) <- c (
"GDPG" ,
"INF" ,
"IR" ,
"EX" ,
"NPL"
)
lag5_selection <- vars:: VARselect (
Y5,
lag.max = 3 ,
type = "const"
)
if (
"SC(n)" %in%
names (
lag5_selection$ selection
)
) {
p5 <- as.integer (
lag5_selection$ selection[
"SC(n)"
]
)
} else {
p5 <- 1 L
}
if (
is.na (p5) ||
p5 < 1
) {
p5 <- 1 L
}
var5 <- vars:: VAR (
Y5,
p = p5,
type = "const"
)
max_root_5 <- max (
vars:: roots (
var5,
modulus = TRUE
)
)
serial_5 <- serial_p_value (
var5
)
robust_ex_ok <- is.finite (
max_root_5
) &&
max_root_5 < 1
if (! robust_ex_ok) {
ex_message <- "Mô hình 5 biến không ổn định."
}
},
error = function (e) {
robust_ex_ok <<- FALSE
ex_message <<- conditionMessage (e)
}
)
9.5. Trạng thái robustness và so sánh IRF
Xem mã R
robustness_status <- data.frame (
Specification = c (
"VAR(2)" ,
"Alternative ordering" ,
"Mở rộng thêm EX"
),
Success = c (
robust_lag2_ok,
robust_order_ok,
robust_ex_ok
),
Max_Root = c (
max_root_p2,
max_root_alt,
max_root_5
),
Portmanteau_p = c (
serial_p2,
serial_alt,
serial_5
),
Note = c (
lag2_message,
order_message,
ex_message
),
stringsAsFactors = FALSE
)
knitr:: kable (
robustness_status |>
dplyr:: mutate (
Max_Root = round (
Max_Root,
4
),
Portmanteau_p = round (
Portmanteau_p,
4
)
),
caption = "Trạng thái các đặc tả robustness"
)
Trạng thái các đặc tả robustness
VAR(2)
TRUE
0.5573
0.3770
Alternative ordering
TRUE
0.5801
0.7111
Mở rộng thêm EX
FALSE
EX chưa đạt ADF 5%; không chạy mô hình 5 biến ở mức.
Xem mã R
write_csv_utf8 (
robustness_status,
"13_Robustness_Status.csv"
)
baseline_irf_simple <- irf_table |>
dplyr:: select (
Shock,
Horizon,
IRF
) |>
dplyr:: mutate (
Specification = "Baseline"
)
robust_irf_list <- list (
baseline_irf_simple
)
if (robust_lag2_ok) {
robust_irf_list[[length (robust_irf_list) + 1 ]] <- dplyr:: bind_rows (
extract_point_irf (
var_p2,
"GDPG" ,
"Cú sốc tăng trưởng GDP" ,
"VAR(2)"
),
extract_point_irf (
var_p2,
"INF" ,
"Cú sốc lạm phát" ,
"VAR(2)"
),
extract_point_irf (
var_p2,
"IR" ,
"Cú sốc lãi suất" ,
"VAR(2)"
)
)
}
if (robust_order_ok) {
robust_irf_list[[length (robust_irf_list) + 1 ]] <- dplyr:: bind_rows (
extract_point_irf (
var_alt,
"GDPG" ,
"Cú sốc tăng trưởng GDP" ,
"Alternative ordering"
),
extract_point_irf (
var_alt,
"INF" ,
"Cú sốc lạm phát" ,
"Alternative ordering"
),
extract_point_irf (
var_alt,
"IR" ,
"Cú sốc lãi suất" ,
"Alternative ordering"
)
)
}
if (robust_ex_ok) {
robust_irf_list[[length (robust_irf_list) + 1 ]] <- dplyr:: bind_rows (
extract_point_irf (
var5,
"GDPG" ,
"Cú sốc tăng trưởng GDP" ,
"Mở rộng thêm EX"
),
extract_point_irf (
var5,
"INF" ,
"Cú sốc lạm phát" ,
"Mở rộng thêm EX"
),
extract_point_irf (
var5,
"IR" ,
"Cú sốc lãi suất" ,
"Mở rộng thêm EX"
)
)
}
robust_irf <- dplyr:: bind_rows (
robust_irf_list
)
p_robust <- ggplot2:: ggplot (
robust_irf,
ggplot2:: aes (
x = Horizon,
y = IRF,
linetype = Specification
)
) +
ggplot2:: geom_hline (
yintercept = 0 ,
linewidth = 0.4
) +
ggplot2:: geom_line (
linewidth = 0.9
) +
ggplot2:: facet_wrap (
~ Shock,
ncol = 1 ,
scales = "free_y"
) +
ggplot2:: scale_x_continuous (
breaks = 0 : H
) +
ggplot2:: labs (
title = "Kiểm định độ vững của IRF NPL" ,
subtitle = "So sánh baseline với các đặc tả thay thế" ,
x = "Số quý sau cú sốc" ,
y = "Phản ứng NPL" ,
linetype = "Đặc tả"
)
p_robust
Xem mã R
ggplot2:: ggsave (
file.path (
output_dir,
"13_Robustness_IRF.png"
),
plot = p_robust,
width = 10 ,
height = 7 ,
dpi = 300
)
write_csv_utf8 (
robust_irf,
"13_Robustness_IRF.csv"
)
10. Stress Testing
Kịch bản được xây dựng từ phân phối các structural shocks của recursive SVAR:
Baseline: dự báo VAR không cộng thêm cú sốc.
Adverse: shock GDPG ở percentile 5%, shock INF và IR ở percentile 95%.
Severe: shock GDPG ở mức thấp nhất mẫu; shock INF và IR ở mức cao nhất mẫu.
Shock giảm dần trong 4 quý theo (1,;0.75,;0.50,;0.25).
Các con số này là đơn vị structural shock , không phải trực tiếp là số điểm phần trăm GDP/INF/IR.
10.1. Baseline forecast
Xem mã R
H_stress <- 8
forecast_base <- predict (
var_model,
n.ahead = H_stress,
ci = 0.95
)
if (
is.null (
forecast_base$ fcst$ NPL
)
) {
stop (
"predict(VAR) không trả về forecast cho NPL."
)
}
baseline_npl <- as.numeric (
forecast_base$ fcst$ NPL[
,
"fcst"
]
)
baseline_lower <- as.numeric (
forecast_base$ fcst$ NPL[
,
"lower"
]
)
baseline_upper <- as.numeric (
forecast_base$ fcst$ NPL[
,
"upper"
]
)
if (
length (baseline_npl) != H_stress
) {
stop (
"Baseline forecast không có đủ 8 quý."
)
}
forecast_quarters <- next_quarters (
tail (
df$ Quarter,
1
),
H_stress
)
10.2. Structural shocks lịch sử
Xem mã R
if (
rcond (
B_recursive
) < 1e-10
) {
stop (
"B_recursive gần suy biến; không tính structural shocks."
)
}
structural_shocks <- t (
solve (
B_recursive,
t (U)
)
)
colnames (
structural_shocks
) <- colnames (Y)
shock_summary <- data.frame (
Variable = colnames (
structural_shocks
),
Mean = apply (
structural_shocks,
2 ,
mean
),
SD = apply (
structural_shocks,
2 ,
stats:: sd
),
P05 = apply (
structural_shocks,
2 ,
stats:: quantile,
probs = 0.05 ,
names = FALSE
),
P95 = apply (
structural_shocks,
2 ,
stats:: quantile,
probs = 0.95 ,
names = FALSE
),
Min = apply (
structural_shocks,
2 ,
min
),
Max = apply (
structural_shocks,
2 ,
max
),
stringsAsFactors = FALSE
)
knitr:: kable (
shock_summary |>
dplyr:: mutate (
dplyr:: across (
c (
Mean,
SD,
P05,
P95,
Min,
Max
),
~ round (.x, 4 )
)
),
caption = "Phân phối structural shocks lịch sử"
)
Phân phối structural shocks lịch sử
GDPG
GDPG
0
0.9623
-1.4056
1.4113
-1.9352
2.2683
INF
INF
0
0.9623
-1.5973
1.6725
-2.9166
1.9645
IR
IR
0
0.9623
-1.5557
1.7475
-2.4728
2.0340
NPL
NPL
0
0.9623
-1.3947
1.7094
-2.1613
3.2423
Xem mã R
write_csv_utf8 (
shock_summary,
"14_structural_shock_summary.csv"
)
10.3. Xây dựng kịch bản
Xem mã R
GDP_P05 <- as.numeric (
stats:: quantile (
structural_shocks[
,
"GDPG"
],
probs = 0.05 ,
names = FALSE
)
)
INF_P95 <- as.numeric (
stats:: quantile (
structural_shocks[
,
"INF"
],
probs = 0.95 ,
names = FALSE
)
)
IR_P95 <- as.numeric (
stats:: quantile (
structural_shocks[
,
"IR"
],
probs = 0.95 ,
names = FALSE
)
)
GDP_MIN <- min (
structural_shocks[
,
"GDPG"
]
)
INF_MAX <- max (
structural_shocks[
,
"INF"
]
)
IR_MAX <- max (
structural_shocks[
,
"IR"
]
)
decay <- c (
1 ,
0.75 ,
0.50 ,
0.25 ,
rep (
0 ,
H_stress - 4
)
)
shock_adverse <- matrix (
0 ,
nrow = H_stress,
ncol = K,
dimnames = list (
forecast_quarters,
colnames (Y)
)
)
shock_severe <- shock_adverse
shock_adverse[
,
"GDPG"
] <- GDP_P05 *
decay
shock_adverse[
,
"INF"
] <- INF_P95 *
decay
shock_adverse[
,
"IR"
] <- IR_P95 *
decay
shock_severe[
,
"GDPG"
] <- GDP_MIN *
decay
shock_severe[
,
"INF"
] <- INF_MAX *
decay
shock_severe[
,
"IR"
] <- IR_MAX *
decay
scenario_assumptions <- data.frame (
Scenario = c (
"Adverse" ,
"Severe"
),
GDP_Shock = c (
GDP_P05,
GDP_MIN
),
INF_Shock = c (
INF_P95,
INF_MAX
),
IR_Shock = c (
IR_P95,
IR_MAX
),
stringsAsFactors = FALSE
)
knitr:: kable (
scenario_assumptions |>
dplyr:: mutate (
dplyr:: across (
c (
GDP_Shock,
INF_Shock,
IR_Shock
),
~ round (.x, 4 )
)
),
caption = "Giá trị shock ban đầu của các kịch bản"
)
Giá trị shock ban đầu của các kịch bản
Adverse
-1.4056
1.6725
1.7475
Severe
-1.9352
1.9645
2.0340
Xem mã R
write_csv_utf8 (
scenario_assumptions,
"14_Stress_Scenario_Assumptions.csv"
)
10.4. Truyền shock qua recursive SVAR
Psi() chứa các structural response matrices (_0,_1,) theo Cholesky identification.
Xem mã R
Psi_stress <- vars:: Psi (
var_model,
nstep = H_stress
)
if (
length (
dim (
Psi_stress
)
) != 3 ||
dim (
Psi_stress
)[1 ] != K ||
dim (
Psi_stress
)[2 ] != K ||
dim (
Psi_stress
)[3 ] < H_stress + 1
) {
stop (
"Psi(VAR) không có kích thước K x K x (H+1)."
)
}
dimnames (Psi_stress) <- list (
colnames (Y),
colnames (Y),
NULL
)
stress_response <- function (
psi_array,
shock_path,
response_name
) {
Hh <- nrow (
shock_path
)
response_index <- match (
response_name,
rownames (
psi_array[
,
,
1
]
)
)
if (
is.na (
response_index
)
) {
stop (
paste0 (
"Không tìm thấy response " ,
response_name,
" trong Psi."
)
)
}
deviation <- numeric (
Hh
)
for (
h in seq_len (Hh)
) {
for (
j in seq_len (h)
) {
lag_index <- h - j
theta <- psi_array[
,
,
lag_index + 1
]
deviation[h] <-
deviation[h] +
as.numeric (
theta[
response_index,
] %*%
shock_path[
j,
]
)
}
}
deviation
}
adverse_dev <- stress_response (
Psi_stress,
shock_adverse,
"NPL"
)
severe_dev <- stress_response (
Psi_stress,
shock_severe,
"NPL"
)
if (
any (
! is.finite (
c (
adverse_dev,
severe_dev
)
)
)
) {
stop (
"Stress response chứa giá trị không hữu hạn."
)
}
10.5. Kết quả Stress Testing
Xem mã R
stress_results <- data.frame (
Horizon = 1 : H_stress,
Quarter = forecast_quarters,
Baseline_NPL = baseline_npl,
Adverse_NPL =
baseline_npl +
adverse_dev,
Severe_NPL =
baseline_npl +
severe_dev,
Baseline_Lower_95 =
baseline_lower,
Baseline_Upper_95 =
baseline_upper,
stringsAsFactors = FALSE
)
stress_results$ Adverse_Gap <-
stress_results$ Adverse_NPL -
stress_results$ Baseline_NPL
stress_results$ Severe_Gap <-
stress_results$ Severe_NPL -
stress_results$ Baseline_NPL
knitr:: kable (
stress_results |>
dplyr:: mutate (
dplyr:: across (
c (
Baseline_NPL,
Adverse_NPL,
Severe_NPL,
Baseline_Lower_95,
Baseline_Upper_95,
Adverse_Gap,
Severe_Gap
),
~ round (.x, 4 )
)
),
caption = "Quỹ đạo NPL theo các kịch bản Stress Testing"
)
Quỹ đạo NPL theo các kịch bản Stress Testing
1
2026Q1
2.1422
2.2546
2.2796
1.9409
2.3434
0.1124
0.1375
2
2026Q2
2.0770
2.4122
2.4975
1.7939
2.3602
0.3352
0.4204
3
2026Q3
2.0284
2.4783
2.5918
1.7059
2.3509
0.4499
0.5634
4
2026Q4
2.0022
2.4428
2.5507
1.6651
2.3392
0.4407
0.5486
5
2027Q1
1.9898
2.3325
2.4130
1.6480
2.3317
0.3426
0.4231
6
2027Q2
1.9843
2.2018
2.2494
1.6410
2.3276
0.2175
0.2651
7
2027Q3
1.9819
2.1052
2.1302
1.6382
2.3256
0.1233
0.1483
8
2027Q4
1.9810
2.0443
2.0564
1.6372
2.3247
0.0633
0.0754
Xem mã R
write_csv_utf8 (
stress_results,
"15_Stress_Testing_NPL.csv"
)
10.6. Đồ thị Stress Testing
Xem mã R
stress_long <- stress_results |>
dplyr:: select (
Horizon,
Baseline_NPL,
Adverse_NPL,
Severe_NPL
) |>
tidyr:: pivot_longer (
cols = - Horizon,
names_to = "Scenario" ,
values_to = "NPL"
) |>
dplyr:: mutate (
Scenario = dplyr:: recode (
Scenario,
Baseline_NPL = "Baseline" ,
Adverse_NPL = "Adverse" ,
Severe_NPL = "Severe"
)
)
p_stress <- ggplot2:: ggplot () +
ggplot2:: geom_ribbon (
data = stress_results,
ggplot2:: aes (
x = Horizon,
ymin = Baseline_Lower_95,
ymax = Baseline_Upper_95
),
alpha = 0.12
) +
ggplot2:: geom_line (
data = stress_long,
ggplot2:: aes (
x = Horizon,
y = NPL,
linetype = Scenario,
group = Scenario
),
linewidth = 1
) +
ggplot2:: geom_point (
data = stress_long,
ggplot2:: aes (
x = Horizon,
y = NPL,
shape = Scenario
),
size = 2
) +
ggplot2:: scale_x_continuous (
breaks = 1 : H_stress,
labels = forecast_quarters
) +
ggplot2:: labs (
title = "Quỹ đạo NPL dưới các kịch bản Stress Testing" ,
subtitle = "Vùng mờ: khoảng dự báo 95% của baseline" ,
x = "Quý" ,
y = "NPL (%)" ,
linetype = "Kịch bản" ,
shape = "Kịch bản"
) +
ggplot2:: theme (
axis.text.x = ggplot2:: element_text (
angle = 45 ,
hjust = 1
)
)
p_stress
Xem mã R
ggplot2:: ggsave (
file.path (
output_dir,
"15_Stress_Testing_NPL.png"
),
plot = p_stress,
width = 10 ,
height = 6 ,
dpi = 300
)
10.7. Peak NPL
Xem mã R
stress_summary <- data.frame (
Scenario = c (
"Baseline" ,
"Adverse" ,
"Severe"
),
Peak_NPL = c (
max (
stress_results$ Baseline_NPL
),
max (
stress_results$ Adverse_NPL
),
max (
stress_results$ Severe_NPL
)
),
Peak_Quarter = c (
stress_results$ Quarter[
which.max (
stress_results$ Baseline_NPL
)
],
stress_results$ Quarter[
which.max (
stress_results$ Adverse_NPL
)
],
stress_results$ Quarter[
which.max (
stress_results$ Severe_NPL
)
]
),
End_NPL = c (
tail (
stress_results$ Baseline_NPL,
1
),
tail (
stress_results$ Adverse_NPL,
1
),
tail (
stress_results$ Severe_NPL,
1
)
),
Max_Gap_vs_Baseline = c (
0 ,
max (
stress_results$ Adverse_Gap
),
max (
stress_results$ Severe_Gap
)
),
stringsAsFactors = FALSE
)
knitr:: kable (
stress_summary |>
dplyr:: mutate (
dplyr:: across (
c (
Peak_NPL,
End_NPL,
Max_Gap_vs_Baseline
),
~ round (.x, 4 )
)
),
caption = "Tóm tắt Stress Testing"
)
Tóm tắt Stress Testing
Baseline
2.1422
2026Q1
1.9810
0.0000
Adverse
2.4783
2026Q3
2.0443
0.4499
Severe
2.5918
2026Q3
2.0564
0.5634
Xem mã R
write_csv_utf8 (
stress_summary,
"16_Stress_Testing_Summary.csv"
)
11. Lưu mô hình và thông tin tái lập
Xem mã R
saveRDS (
var_model,
file.path (
output_dir,
"VAR_model.rds"
)
)
saveRDS (
B_recursive,
file.path (
output_dir,
"Recursive_SVAR_B_matrix.rds"
)
)
if (svar_check_ok) {
saveRDS (
svar_check,
file.path (
output_dir,
"SVAR_crosscheck_model.rds"
)
)
}
session_text <- capture.output (
sessionInfo ()
)
writeLines (
session_text,
file.path (
output_dir,
"sessionInfo.txt"
)
)
cat (
"Pipeline hoàn thành. Kết quả đã lưu tại:" ,
normalizePath (
output_dir,
winslash = "/" ,
mustWork = TRUE
),
" \n "
)
Pipeline hoàn thành. Kết quả đã lưu tại: C:/Users/Dell/Documents/output_chapter4
12. Checklist đầu ra
Sau khi Render thành công, thư mục output_chapter4 sẽ có:
Thống kê mô tả và đồ thị chuỗi thời gian.
Ma trận tương quan.
ADF, PP, KPSS.
Lựa chọn độ trễ.
Hệ số VAR và roots.
Portmanteau, ARCH, Jarque–Bera, Skewness, Kurtosis.
OLS-CUSUM và bảng kiểm định.
Ma trận B của recursive SVAR và đối chiếu SVAR().
IRF bootstrap 95% và peak response.
FEVD tại toàn bộ horizon, đặc biệt Q1, Q4, Q8.
Robustness theo lag, ordering và biến EX.
Stress Testing Baseline–Adverse–Severe.
Peak NPL và gap so với baseline.
File mô hình .rds và sessionInfo.txt.
ADF: tau2 < critical value 5% → bác bỏ nghiệm đơn vị.
PP: p < 0,05 → bác bỏ nghiệm đơn vị.
KPSS: p > 0,05 → không bác bỏ tính dừng.
Roots: mọi modulus < 1 → VAR ổn định.
Portmanteau: p > 0,05 → chưa có bằng chứng tự tương quan phần dư.
ARCH: p > 0,05 → chưa có bằng chứng ARCH đa biến.
Jarque–Bera: p > 0,05 → chưa bác bỏ phân phối chuẩn.
OLS-CUSUM: p > 0,05 và đường nằm trong biên → chưa có bằng chứng bất ổn tham số.
IRF: CI 95% không chứa 0 → phản ứng có ý nghĩa thống kê tại horizon đó.
FEVD: đọc tỷ trọng shock GDPG, INF, IR và own-NPL shock.
Stress Testing: tập trung Peak NPL, Peak Quarter và Max Gap vs Baseline.