Celem projektu jest sprawdzenie, czy rozkład zachorowań na COVID-19 w prowincjach Hiszpanii ma charakter przestrzenny, oraz identyfikacja jego determinant klimatycznych, ekonomicznych i demograficznych. Raport prowadzi przez pełną ścieżkę: od wizualizacji i statystyk przestrzennych, przez model bazowy i jego diagnostykę, po modele panelowe, efekty i analizę odporności.
Zjawisko ma wyraźny charakter przestrzenny: choroba zakaźna rozprzestrzenia się przez kontakty międzyludzkie, intensywniejsze między sąsiadującymi jednostkami. Dobór determinant (oparty na przeglądzie literatury): temperatura i wilgotność (sezonowość transmisji wirusów oddechowych), PKB per capita (mobilność), gęstość zaludnienia (intensywność kontaktów) oraz odsetek osób 65+ (wykrywalność i ciężkość przypadków). Zmienne pogodowe opóźniono o 8 dni (inkubacja ~5-6 dni wg WHO + czas na test i raportowanie).
# Folder z danymi: zostaw pusty ciag (autowykrycie folderu tego .Rmd)
# albo wpisz pelna sciezke, np. "C:/Users/Ty/dane".
data_dir_manual <- ""
find_data <- function() {
kand <- character(0)
rmd <- tryCatch(knitr::current_input(dir = TRUE), error = function(e) NA)
if (!is.na(rmd)) kand <- c(kand, dirname(rmd))
kand <- unique(c(kand, getwd(), "."))
for (d in kand) if (file.exists(file.path(d, "provinces_spain.RData"))) return(d)
stop("Nie znaleziono 'provinces_spain.RData'. Ustaw data_dir_manual.")
}
data_dir <- if (nzchar(data_dir_manual)) data_dir_manual else find_data()
# Ladna tabela wspolczynnikow modelu panelowego splm (lambda + bety)
tab_panel <- function(m) {
ct <- tryCatch(summary(m)$CoefTable, error = function(e) NULL)
if (is.null(ct)) ct <- tryCatch(coef(summary(m)), error = function(e) NULL)
if (is.null(ct)) ct <- as.matrix(coef(m))
lam <- tryCatch(as.numeric(m$arcoef)[1], error = function(e) NA)
if (!is.na(lam) && ncol(ct) >= 1) {
lr <- matrix(NA_real_, 1, ncol(ct),
dimnames = list("lambda (lag przestrz.)", colnames(ct)))
lr[1, 1] <- lam; ct <- rbind(lr, ct)
}
round(ct, 4)
}
library(sf) # struktury danych przestrzennych (.shp/.shx/.dbf/.prj)
library(tidyverse) # manipulacja i czyszczenie danych
library(spdep) # macierze wag, autokorelacja przestrzenna, LISA, Gi*
library(spatialreg) # modele przekrojowe SAR/SEM/SDM/SARAR + efekty (impacts)
library(tmap) # zaawansowana kartografia
library(plm) # klasyczna ekonometria panelowa
library(splm) # przestrzenne modele panelowe
library(lmtest) # testy diagnostyczne
library(car) # współliniowość (VIF)
library(RColorBrewer) # palety barw do map
sf_use_s2(FALSE) # poprawna topologia sferyczna dla danych w lon/lat
tmap_mode("plot") # statyczne mapy; "view" daje mapy interaktywne (Leaflet)
# UWAGA O WERSJI tmap: poniższy kod kartograficzny korzysta ze składni tmap 3.x
# (argumenty: style=, palette=, title=, tm_layout(legend.outside=...)).
# W tmap >= 4.0 składnia uległa zmianie (fill=, fill.scale=, tm_legend()).
# Aby uniknąć błędów/ostrzeżeń, zalecane jest środowisko z tmap 3.3-x:
if (utils::packageVersion("tmap") >= "4.0") {
message("UWAGA: wykryto tmap >= 4.0. Kod map pisano pod tmap 3.x.\n",
"Jeśli mapy nie renderują się poprawnie, zainstaluj tmap 3.3-3:\n",
" remotes::install_version('tmap', version = '3.3-3')")
}
# UWAGA: ustaw katalog roboczy na folder z plikami .RData, np.:
# setwd("C:/.../dane_projekt")
# --- FUNKCJA POMOCNICZA: ręczne (dokładne) średnie efekty przestrzenne ---
# Niezależny od wersji pakietów odpowiednik impacts() (LeSage-Pace). Działa dla
# obiektów sarlm (przekrój) i spml (panel). Mnożnik S = (I - rho*W)^(-1):
# CAŁKOWITY = beta * (1/n)*suma(S); BEZPOŚREDNI = beta * (1/n)*ślad(S);
# POŚREDNI = całkowity - bezpośredni. Bez części AR (SEM/OLS): efekt = beta.
impacts_reczne <- function(model, listw) {
W <- listw2mat(listw); n <- nrow(W); Imat <- diag(n)
rho <- NA
if (!is.null(model$arcoef)) rho <- as.numeric(model$arcoef)[1] # splm (lag)
else if (!is.null(model$rho)) rho <- as.numeric(model$rho) # sarlm (lag)
b <- model$coefficients; if (is.null(b)) b <- coef(model)
b <- b[!grepl("Intercept|^rho$|^lambda$", names(b))] # tylko nachylenia
if (is.na(rho) || length(rho) == 0) {
direct <- b; total <- b; indirect <- rep(0, length(b))
} else {
S <- solve(Imat - rho * W)
direct <- b * (sum(diag(S)) / n)
total <- b * (sum(S) / n)
indirect <- total - direct
}
data.frame(Zmienna = names(b),
Bezposredni = as.numeric(direct),
Posredni = as.numeric(indirect),
Calkowity = as.numeric(total), row.names = NULL)
}Analizę prowadzimy dla 47 prowincji lądowych Hiszpanii (z 50 usuwamy 3 wyspiarskie bez granicy lądowej) w oknie 30 dni pierwszej fali. Sąsiedztwo definiujemy macierzą Queen I rzędu (wspólna granica), standaryzowaną wierszowo — wydruk poniżej podsumowuje jej strukturę.
load(file.path(data_dir, "provinces_spain.RData"))
load(file.path(data_dir, "covid19_spain_1.RData"))
load(file.path(data_dir, "covid19_spain_2.RData")) # zbiór do analizy odporności
# 1.1 Naprawa geometrii + identyfikacja jednostek izolowanych (wysp)
provinces_valid <- st_make_valid(provinces_spain) %>% arrange(province)
nb_temp <- poly2nb(provinces_valid, queen = TRUE)
islands <- which(card(nb_temp) == 0)
prowincje_bez_wysp <- provinces_valid$province[-islands]
cat("Liczba prowincji wchodzących do analizy:",
length(prowincje_bez_wysp), "\n")Liczba prowincji wchodzących do analizy: 47
# 1.2 Funkcja budująca bazę panelową dla DOWOLNEGO zbioru COVID
# (dzięki temu identyczna obróbka dla zbioru 1 i zbioru 2 = pełna porównywalność)
buduj_panel <- function(covid_df) {
left_join(
covid_df,
st_drop_geometry(provinces_valid) %>%
select(province, Population, Density, Older, Median_Age,
Male2Female, GDPpc, Transit, Altitude, Coast, Area),
by = "province"
) %>%
filter(province %in% prowincje_bez_wysp) %>%
mutate(
Density_numeric = as.numeric(Density),
Older_numeric = as.numeric(Older),
Transit_f = factor(Transit, labels = c("Brak metra", "Metro"))
) %>%
arrange(province, Date)
}
baza_panelowa <- buduj_panel(covid19_spain_1) # zbiór bazowy
baza_panelowa_2 <- buduj_panel(covid19_spain_2) # zbiór do robustness
# 1.3 Baza przekrojowa (jeden punkt czasowy: ostatni dzień próby = 2020-04-11)
DATA_PRZEKROJU <- as.Date("2020-04-11")
baza_przekroj <- left_join(
provinces_valid,
covid19_spain_1 %>% filter(Date == DATA_PRZEKROJU),
by = "province"
) %>%
filter(province %in% prowincje_bez_wysp) %>%
mutate(
Density_numeric = as.numeric(Density),
Older_numeric = as.numeric(Older)
) %>%
arrange(province)
# 1.4 Macierz wag przestrzennych pierwszego rzędu (Queen), standaryzowana wierszowo
# Budowana z bazy przekrojowej (47 jednostek, ułożonych wg province) i
# wykorzystywana TAKŻE w modelach panelowych (zgodność liczby i kolejności
# jednostek z układem panelu, który również jest sortowany wg province).
nb <- poly2nb(baza_przekroj, queen = TRUE)
lw <- nb2listw(nb, style = "W", zero.policy = TRUE)
cat("\nPODSUMOWANIE MACIERZY WAG (Queen, std. wierszowa)\n")
PODSUMOWANIE MACIERZY WAG (Queen, std. wierszowa)
Characteristics of weights list object:
Neighbour list object:
Number of regions: 47
Number of nonzero links: 222
Percentage nonzero weights: 10.0498
Average number of links: 4.723404
Weights style: W
Weights constants summary:
n nn S0 S1 S2
W 47 2209 47 21.39235 193.2818
Otrzymujemy spójną sieć: 222 powiązania, średnio 4,72 sąsiada na prowincję. To fundament wszystkich dalszych statystyk przestrzennych; większa gęstość połączeń w centrum kraju zapowiada silniejszy potencjał efektów pośrednich w rdzeniu.
Sześć wizualizacji buduje obraz zjawiska. (1) Mapa sieci sąsiedztwa pokazuje topologię powiązań. (2) Kartogram zachorowalności na 11.04 — wartości od 56 do 1149/100 tys. (3) Sześć map determinant (temperatura, wilgotność, PKB, gęstość, odsetek 65+). (4) Ranking 15 prowincji. (5) Macierz korelacji. (6) Dynamika w ośmiu dniach.
# --- 2.1 Graf sąsiedztwa (struktura macierzy wag) ---
centroidy <- st_coordinates(st_centroid(st_geometry(baza_przekroj)))
plot(st_geometry(baza_przekroj), border = "grey80",
main = "Struktura sąsiedztwa prowincji (macierz Queen, I rzędu)")
plot(nb, coords = centroidy, add = TRUE, col = "firebrick", lwd = 0.8,
points = TRUE, pch = 19, cex = 0.5)Sieć sąsiedztwa 47 prowincji (Queen) — połączenia gęstsze w centrum, gdzie potencjał efektów pośrednich jest największy.
# --- 2.2 Kartogram zmiennej objaśnianej (zachorowalność) ---
mapa_chorob <- tm_shape(baza_przekroj) +
tm_polygons("Incidence", style = "jenks", palette = "YlOrRd", n = 5,
title = "Zachorowalność\n(na 100 tys.)") +
tm_layout(main.title = paste0("Rozkład przestrzenny COVID-19 (",
format(DATA_PRZEKROJU, "%d.%m.%Y"), ")"),
legend.outside = TRUE, main.title.size = 1.0)
print(mapa_chorob)Zachorowalność 11.04.2020 (56–1149/100 tys.): najwyższe wartości tworzą zwarty pas w centrum kraju, nie pojedynczy punkt.
# --- 2.3 Kartogramy WSZYSTKICH zmiennych objaśniających (jeden panel) ---
# Wizualne sprawdzenie, czy determinanty mają własną strukturę przestrzenną.
mapa_determinanta <- function(zmienna, tytul, paleta = "viridis") {
tm_shape(baza_przekroj) +
tm_polygons(zmienna, style = "quantile", palette = paleta, n = 5,
title = tytul) +
tm_layout(main.title = tytul, main.title.size = 0.9,
legend.outside = TRUE, legend.outside.position = "right",
legend.outside.size = 0.35, legend.title.size = 0.8,
legend.text.size = 0.6, inner.margins = c(0.02, 0.02, 0.02, 0.02))
}
m1 <- mapa_determinanta("Mean_Temp_lag8", "Temperatura (lag8, °C)")
m2 <- mapa_determinanta("Humidity_lag8", "Wilgotność (lag8, %)")
m3 <- mapa_determinanta("GDPpc", "PKB per capita (EUR)")
m4 <- mapa_determinanta("Density_numeric", "Gęstość zaludnienia (os./km²)")
m5 <- mapa_determinanta("Older_numeric", "Odsetek osób 65+ (%)")
m6 <- mapa_determinanta("Median_Age", "Mediana wieku (lata)")
mapa_determinanty <- tmap_arrange(m1, m2, m3, m4, m5, m6, ncol = 3, nrow = 2)
print(mapa_determinanty)Sześć determinant — każda ma własną strukturę przestrzenną (np. temperatura rośnie ku południu, PKB koncentruje się na północy).
# --- 2.4 Ranking prowincji wg zachorowalności (top 15) ---
ranking_df <- baza_przekroj %>%
st_drop_geometry() %>%
arrange(desc(Incidence)) %>%
slice(1:15)
par(mar = c(5, 11, 4, 2)) # szeroki lewy margines: długie nazwy (np. "Santa Cruz...")
bp <- barplot(rev(ranking_df$Incidence),
names.arg = rev(as.character(ranking_df$province)), # FIX: faktor -> tekst
horiz = TRUE, las = 1, col = "tomato", cex.names = 0.8,
main = "15 prowincji o najwyższej zachorowalności (11.04.2020)",
xlab = "Zachorowalność na 100 tys. mieszkańców",
xlim = c(0, max(ranking_df$Incidence) * 1.12))
text(rev(ranking_df$Incidence), bp, labels = round(rev(ranking_df$Incidence)),
pos = 4, cex = 0.7) # wartości przy słupkachTop 15 prowincji: w ujęciu na mieszkańca prowadzą małe prowincje wewnętrzne (Segovia, Soria), a Madryt jest dopiero 6.
par(mar = c(5, 4, 4, 2))
# --- 2.5 Macierz korelacji determinant (kontrola współliniowości) ---
zmienne_X <- baza_przekroj %>%
st_drop_geometry() %>%
transmute(Incidence, Mean_Temp_lag8, Humidity_lag8, GDPpc,
Density_numeric, Older_numeric, Median_Age)
macierz_kor <- cor(zmienne_X, use = "complete.obs")
cat("\nMACIERZ KORELACJI (zmienne w poziomach)\n")
MACIERZ KORELACJI (zmienne w poziomach)
Incidence Mean_Temp_lag8 Humidity_lag8 GDPpc Density_numeric
Incidence 1.00 -0.56 0.11 0.28 -0.03
Mean_Temp_lag8 -0.56 1.00 0.04 -0.47 0.19
Humidity_lag8 0.11 0.04 1.00 -0.12 -0.10
GDPpc 0.28 -0.47 -0.12 1.00 0.46
Density_numeric -0.03 0.19 -0.10 0.46 1.00
Older_numeric -0.15 -0.12 -0.02 0.08 0.02
Median_Age 0.23 -0.61 -0.05 0.06 -0.24
Older_numeric Median_Age
Incidence -0.15 0.23
Mean_Temp_lag8 -0.12 -0.61
Humidity_lag8 -0.02 -0.05
GDPpc 0.08 0.06
Density_numeric 0.02 -0.24
Older_numeric 1.00 0.05
Median_Age 0.05 1.00
# Prosta wizualizacja macierzy korelacji bez dodatkowych pakietów:
image(1:ncol(macierz_kor), 1:nrow(macierz_kor),
t(macierz_kor[nrow(macierz_kor):1, ]),
axes = FALSE, xlab = "", ylab = "",
col = colorRampPalette(c("steelblue", "white", "firebrick"))(20),
main = "Macierz korelacji determinant")
axis(1, at = 1:ncol(macierz_kor), labels = colnames(macierz_kor),
las = 2, cex.axis = 0.7)
axis(2, at = 1:nrow(macierz_kor), labels = rev(rownames(macierz_kor)),
las = 1, cex.axis = 0.7)Macierz korelacji: najsilniejszy (ujemny) związek z zachorowalnością ma temperatura; pozostałe są słabe — brak szkodliwej współliniowości.
# --- 2.6 Panele czasowe: rozprzestrzenianie się epidemii w czasie ---
# Wybór 8 reprezentatywnych dni z 30-dniowego okna, aby panel był czytelny.
dni_panel <- sort(unique(covid19_spain_1$Date))
dni_wybrane <- dni_panel[round(seq(1, length(dni_panel), length.out = 8))]
panel_czasowy <- left_join(
provinces_valid,
covid19_spain_1 %>% filter(Date %in% dni_wybrane),
by = "province"
) %>%
filter(province %in% prowincje_bez_wysp) %>%
mutate(Dzien = factor(format(Date, "%d.%m.%Y"),
levels = format(sort(dni_wybrane), "%d.%m.%Y")))
mapa_dynamika <- tm_shape(panel_czasowy) +
tm_polygons("Incidence", style = "fixed",
breaks = c(0, 50, 150, 300, 500, 800, 1200),
palette = "YlOrRd", title = "Zachorowalność\n(na 100 tys.)") +
tm_facets(by = "Dzien", ncol = 4, free.coords = FALSE) +
tm_layout(main.title = "Dynamika przestrzenna pandemii COVID-19 (8 wybranych dni)",
main.title.size = 1.0,
legend.outside = TRUE, legend.outside.position = "right",
legend.outside.size = 0.18, panel.label.size = 0.9)
print(mapa_dynamika)Dynamika w 8 dniach: od niemal zera (13.03) do zwartego pasa centralnego (11.04) — widoczna dyfuzja od sąsiada do sąsiada.
Zachorowalność tworzy zwarty pas w centrum kraju (obie Kastylie), a nie pojedynczy punkt. W ujęciu na mieszkańca prowadzą małe prowincje wewnętrzne (Segovia, Soria, Ciudad Real), a Madryt jest dopiero 6. Każda determinanta ma własną strukturę przestrzenną, a najsilniejszym (ujemnym) korelatem zachorowalności jest temperatura. Dynamika ujawnia narastanie ogniska „od sąsiada do sąsiada” — to pierwsza, opisowa przesłanka za modelem przestrzennym.
Najpierw statystyka globalna (I Morana + test Monte Carlo), potem jej przebieg w czasie, wykres rozproszenia Morana, a następnie statystyki lokalne: mapa klastrów LISA, mapa istotności, wersja po korekcie FDR oraz Getis-Ord Gi*. Na końcu test odporności na macierz wag (kNN).
# --- 3.1 Globalna statystyka I Morana ---
test_morana <- moran.test(baza_przekroj$Incidence, lw, zero.policy = TRUE)
cat("\nGLOBALNY TEST I MORANA (zachorowalność)\n")
GLOBALNY TEST I MORANA (zachorowalność)
Moran I test under randomisation
data: baza_przekroj$Incidence
weights: lw
Moran I statistic standard deviate = 2.6746, p-value = 0.003741
alternative hypothesis: greater
sample estimates:
Moran I statistic Expectation Variance
0.224262216 -0.021739130 0.008460031
# Test permutacyjny Monte Carlo (odporny na odstępstwa od normalności)
set.seed(2024)
moran_mc <- moran.mc(baza_przekroj$Incidence, lw, nsim = 999, zero.policy = TRUE)
cat("\nTEST MONTE CARLO I MORANA (999 permutacji)\n")
TEST MONTE CARLO I MORANA (999 permutacji)
Monte-Carlo simulation of Moran I
data: baza_przekroj$Incidence
weights: lw
number of simulations + 1: 1000
statistic = 0.22426, observed rank = 991, p-value = 0.009
alternative hypothesis: greater
dni_all <- sort(unique(covid19_spain_1$Date))
moran_w_czasie <- sapply(dni_all, function(d) {
x <- covid19_spain_1 %>%
filter(Date == d, province %in% prowincje_bez_wysp) %>%
arrange(province) %>% pull(Incidence)
as.numeric(moran.test(x, lw, zero.policy = TRUE)$estimate[1])
})
pval_w_czasie <- sapply(dni_all, function(d) {
x <- covid19_spain_1 %>%
filter(Date == d, province %in% prowincje_bez_wysp) %>%
arrange(province) %>% pull(Incidence)
as.numeric(moran.test(x, lw, zero.policy = TRUE)$p.value)
})
plot(dni_all, moran_w_czasie, type = "o", pch = 19, col = "firebrick",
xlab = "Data", ylab = "Globalne I Morana",
main = "Dynamika autokorelacji przestrzennej zachorowalności (2020)")
points(dni_all[pval_w_czasie < 0.05], moran_w_czasie[pval_w_czasie < 0.05],
pch = 19, col = "darkgreen", cex = 1.3) # dni z istotnym I (p<0,05)
abline(h = 0, lty = 2, col = "grey50")
legend("topleft", bty = "n", pch = 19, col = c("firebrick", "darkgreen"),
legend = c("I Morana (dzień)", "istotne p<0,05"))Globalne I Morana dla każdego z 30 dni — istotne we wszystkich (punkty zielone); klastrowanie jest trwałe i nasila się w trakcie fali.
I MORANA W CZASIE: zakres i liczba dni istotnych
cat("min I:", round(min(moran_w_czasie), 3),
" max I:", round(max(moran_w_czasie), 3),
" dni istotnych (p<0,05):", sum(pval_w_czasie < 0.05), "z", length(dni_all), "\n")min I: 0.156 max I: 0.243 dni istotnych (p<0,05): 30 z 30
# --- 3.2 Wykres rozproszenia Morana ---
moran.plot(baza_przekroj$Incidence, lw, zero.policy = TRUE, labels = FALSE,
xlab = "Zachorowalność (standaryzowana)",
ylab = "Przestrzenne opóźnienie W·Incidence",
main = "Wykres rozproszenia Morana — COVID-19")Wykres rozproszenia Morana: dodatnie nachylenie i zagęszczenie w ćwiartkach High-High/Low-Low potwierdzają dodatnią autokorelację.
# --- 3.3 Lokalna statystyka LISA: klastry + ISTOTNOŚĆ (dwie mapy) ---
lisa <- localmoran(baza_przekroj$Incidence, lw, zero.policy = TRUE)
z_inc <- as.vector(scale(baza_przekroj$Incidence))
z_lag <- as.vector(scale(lag.listw(lw, baza_przekroj$Incidence, zero.policy = TRUE)))
p_lisa <- lisa[, ncol(lisa)] # ostatnia kolumna = p-value
baza_przekroj <- baza_przekroj %>%
mutate(
LISA_p = p_lisa,
LISA_p_BH = p.adjust(p_lisa, method = "BH"), # korekta FDR (Benjamini-Hochberg)
kwadrant = case_when(
z_inc > 0 & z_lag > 0 ~ "High-High",
z_inc < 0 & z_lag < 0 ~ "Low-Low",
z_inc > 0 & z_lag < 0 ~ "High-Low",
z_inc < 0 & z_lag > 0 ~ "Low-High"
),
klaster_LISA = ifelse(LISA_p < 0.05, kwadrant, "Nieistotny"),
klaster_LISA_BH = ifelse(LISA_p_BH < 0.05, kwadrant, "Nieistotny"),
istotnosc = cut(LISA_p, breaks = c(0, 0.01, 0.05, 0.10, 1),
labels = c("p<0.01", "p<0.05", "p<0.10", "nieistotny"))
)
cat("\nLISA: liczba istotnych jednostek (p<0,05) PRZED i PO korekcie FDR\n")
LISA: liczba istotnych jednostek (p<0,05) PRZED i PO korekcie FDR
Bez korekty : 9
Po korekcie BH: 0
mapa_lisa <- tm_shape(baza_przekroj) +
tm_polygons("klaster_LISA",
palette = c("High-High" = "red", "Low-Low" = "blue",
"High-Low" = "pink", "Low-High" = "lightblue",
"Nieistotny" = "white"),
title = "Typ klastra (LISA)") +
tm_layout(main.title = "Mapa klastrów LISA — COVID-19", legend.outside = TRUE)
print(mapa_lisa)Klastry LISA: ognisko High-High w centralnej Kastylii, Low-High na północy, Low-Low na południu (wskazania surowe, p<0,05).
mapa_istotnosc <- tm_shape(baza_przekroj) +
tm_polygons("istotnosc",
palette = c("p<0.01" = "#006d2c", "p<0.05" = "#41ab5d",
"p<0.10" = "#a1d99b", "nieistotny" = "white"),
title = "Istotność lokalna (p)") +
tm_layout(main.title = "Mapa istotności LISA — COVID-19", legend.outside = TRUE)
print(mapa_istotnosc)Mapa istotności lokalnej: najmocniejsze wskazania (p<0,01) leżą w rdzeniu centralnym i na północy.
mapa_lisa_bh <- tm_shape(baza_przekroj) +
tm_polygons("klaster_LISA_BH",
palette = c("High-High" = "red", "Low-Low" = "blue",
"High-Low" = "pink", "Low-High" = "lightblue",
"Nieistotny" = "white"),
title = "Klaster LISA (FDR-BH)") +
tm_layout(main.title = "Klastry LISA po korekcie FDR (Benjamini-Hochberg)",
legend.outside = TRUE)
print(mapa_lisa_bh)Po korekcie FDR żaden klaster nie jest już istotny (9 do 0) — pewny pozostaje sygnał globalny, nie pojedyncze prowincje.
# --- 3.4 Statystyka Getis-Ord Gi* (gorące i zimne punkty) ---
nb_self <- include.self(nb) # Gi* uwzględnia jednostkę i jej sąsiadów
lw_self <- nb2listw(nb_self, style = "W", zero.policy = TRUE)
gi_star <- localG(baza_przekroj$Incidence, lw_self, zero.policy = TRUE)
baza_przekroj$Gi_star <- as.numeric(gi_star)
mapa_gi <- tm_shape(baza_przekroj) +
tm_polygons("Gi_star", style = "fixed",
breaks = c(-Inf, -1.96, -1.645, 1.645, 1.96, Inf),
palette = c("#2166ac", "#92c5de", "white", "#f4a582", "#b2182b"),
labels = c("Cold spot (1%)", "Cold spot (5%)", "Nieistotny",
"Hot spot (5%)", "Hot spot (1%)"),
title = "Getis-Ord Gi* (z-score)") +
tm_layout(main.title = "Mapa gorących i zimnych punktów (Gi*)",
legend.outside = TRUE)
print(mapa_gi)Getis-Ord Gi*: wyraźny hot-spot (1%) w centralnej Kastylii i cold-spot (1%) w zachodniej Andaluzji — zgodnie z LISA.
coords_knn <- st_coordinates(st_centroid(st_geometry(baza_przekroj)))
knn4 <- knn2nb(knearneigh(coords_knn, k = 4))
lw_knn <- nb2listw(knn4, style = "W", zero.policy = TRUE)
moran_queen <- moran.test(baza_przekroj$Incidence, lw, zero.policy = TRUE)
moran_knn <- moran.test(baza_przekroj$Incidence, lw_knn, zero.policy = TRUE)
cat("\nODPORNOŚĆ I MORANA NA WYBÓR MACIERZY WAG\n")
ODPORNOŚĆ I MORANA NA WYBÓR MACIERZY WAG
porown_W <- data.frame(
Macierz = c("Queen (sąsiedztwo)", "kNN (k=4)"),
I_Morana = round(c(moran_queen$estimate[1], moran_knn$estimate[1]), 4),
p_value = signif(c(moran_queen$p.value, moran_knn$p.value), 3)
)
print(porown_W) Macierz I_Morana p_value
1 Queen (sąsiedztwo) 0.2243 0.00374
2 kNN (k=4) 0.2059 0.00617
Globalne I Morana = 0,224 (p = 0,0037; Monte Carlo p = 0,009) — istotna dodatnia autokorelacja, istotna we wszystkich 30 dniach i odporna na definicję sąsiedztwa (kNN: 0,206). LISA i Gi* zgodnie wskazują hot-spot w centralnej Kastylii i cold-spot w zachodniej Andaluzji. Istotna uwaga: po korekcie FDR żaden klaster lokalny nie pozostaje istotny (9 → 0). Pewny jest więc sygnał globalny, natomiast pojedynczych prowincji-klastrów nie należy przeceniać — to dojrzałe, uczciwe ujęcie.
Estymujemy klasyczny model regresji (tabela współczynników poniżej), badamy przestrzenny rozkład jego reszt na mapie, sprawdzamy założenia (VIF, normalność, homoskedastyczność) oraz wykonujemy testy ex-ante (Rao-Score), które wskazują, czy potrzebny jest człon przestrzenny.
# 4.1 Dobór specyfikacji czasowej (bieżąca vs opóźniona o 8 dni)
# Uzasadnienie merytoryczne: okres inkubacji wg WHO ~5-6 dni + czas na
# test PCR i raportowanie -> opóźnienie 8 dni dla zmiennych pogodowych.
formula_biezaca <- log(Incidence) ~ log(Mean_Temp) + log(Humidity) +
log(GDPpc) + log(Density_numeric) + log(Older_numeric)
formula_lag8 <- log(Incidence) ~ log(Mean_Temp_lag8) + log(Humidity_lag8) +
log(GDPpc) + log(Density_numeric) + log(Older_numeric)
model_ols_biezacy <- lm(formula_biezaca, data = baza_przekroj)
model_ols_lag8 <- lm(formula_lag8, data = baza_przekroj)
cat("\nPORÓWNANIE AIC: SPECYFIKACJA BIEŻĄCA vs OPÓŹNIONA (lag8)\n")
PORÓWNANIE AIC: SPECYFIKACJA BIEŻĄCA vs OPÓŹNIONA (lag8)
AIC (bieżąca): 95.03
AIC (lag8) : 88.18
model_ols_final <- model_ols_lag8
knitr::kable(round(summary(model_ols_final)$coefficients, 4),
col.names = c("Oszacowanie", "Bl. std.", "t", "p-value"),
caption = "Model OLS (lag8): wspolczynniki")| Oszacowanie | Bl. std. | t | p-value | |
|---|---|---|---|---|
| (Intercept) | 2.7039 | 9.9068 | 0.2729 | 0.7863 |
| log(Mean_Temp_lag8) | -1.9928 | 0.5754 | -3.4634 | 0.0013 |
| log(Humidity_lag8) | 1.2409 | 1.5735 | 0.7886 | 0.4349 |
| log(GDPpc) | 0.3780 | 0.6101 | 0.6196 | 0.5390 |
| log(Density_numeric) | -0.0271 | 0.1082 | -0.2502 | 0.8037 |
| log(Older_numeric) | -0.4737 | 0.4634 | -1.0222 | 0.3127 |
WSPÓŁLINIOWOŚĆ (VIF)
log(Mean_Temp_lag8) log(Humidity_lag8) log(GDPpc)
2.594686 1.028790 2.235043
log(Density_numeric) log(Older_numeric)
2.277797 1.011787
TEST NORMALNOŚCI RESZT (Shapiro-Wilk)
Shapiro-Wilk normality test
data: residuals(model_ols_final)
W = 0.97514, p-value = 0.4094
TEST HETEROSKEDASTYCZNOŚCI (Breusch-Pagan)
studentized Breusch-Pagan test
data: model_ols_final
BP = 6.9819, df = 5, p-value = 0.222
# 4.3 Mapa reszt OLS — wizualna motywacja modeli przestrzennych
baza_przekroj$reszty_OLS <- residuals(model_ols_final)
mapa_reszt <- tm_shape(baza_przekroj) +
tm_polygons("reszty_OLS", style = "quantile", palette = "RdBu", midpoint = 0,
title = "Reszty OLS") +
tm_layout(main.title = "Reszty modelu OLS (lag8)", legend.outside = TRUE)
print(mapa_reszt)Mapa reszt OLS: sąsiednie prowincje mają reszty o tym samym znaku — model bez członu przestrzennego pomija autokorelację (I=0,175; p=0,0046).
# Test Morana NA RESZTACH OLS — czy model klasyczny "zostawia" autokorelację?
cat("\nTEST I MORANA NA RESZTACH OLS\n")
TEST I MORANA NA RESZTACH OLS
Global Moran I for regression residuals
data:
model: lm(formula = formula_lag8, data = baza_przekroj)
weights: lw
Moran I statistic standard deviate = 2.6039, p-value = 0.004609
alternative hypothesis: greater
sample estimates:
Observed Moran I Expectation Variance
0.174728163 -0.052191906 0.007594742
# 4.4 Przestrzenne testy mnożnika Lagrange'a / Rao-Score (EX-ANTE)
cat("\nPRZESTRZENNE TESTY RAO-SCORE (EX-ANTE)\n")
PRZESTRZENNE TESTY RAO-SCORE (EX-ANTE)
testy_rs <- lm.RStests(model_ols_final, lw, test = "all", zero.policy = TRUE)
print(summary(testy_rs)) Rao's score (a.k.a Lagrange multiplier) diagnostics for spatial
dependence
data:
model: lm(formula = formula_lag8, data = baza_przekroj)
test weights: lw
statistic parameter p.value
RSerr 3.15255814 1 0.07581 .
RSlag 4.48012960 1 0.03429 *
adjRSerr 0.00035561 1 0.98495
adjRSlag 1.32792706 1 0.24917
SARMA 4.48048520 2 0.10643
---
Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
WSKAZANIE EX-ANTE (odczyt z testów Rao-Score)
rs_tab <- tryCatch(summary(testy_rs)$tests,
error = function(e) tryCatch(t(sapply(testy_rs,
function(x) c(stat = unname(x$statistic),
p = unname(x$p.value)))),
error = function(e2) NULL))
if (!is.null(rs_tab)) print(round(rs_tab, 4)) else print(testy_rs)
Rao's score (a.k.a Lagrange multiplier) diagnostics for spatial
dependence
data:
model: lm(formula = formula_lag8, data = baza_przekroj)
test weights: lw
RSerr = 3.1526, df = 1, p-value = 0.07581
Rao's score (a.k.a Lagrange multiplier) diagnostics for spatial
dependence
data:
model: lm(formula = formula_lag8, data = baza_przekroj)
test weights: lw
RSlag = 4.4801, df = 1, p-value = 0.03429
Rao's score (a.k.a Lagrange multiplier) diagnostics for spatial
dependence
data:
model: lm(formula = formula_lag8, data = baza_przekroj)
test weights: lw
adjRSerr = 0.00035561, df = 1, p-value = 0.985
Rao's score (a.k.a Lagrange multiplier) diagnostics for spatial
dependence
data:
model: lm(formula = formula_lag8, data = baza_przekroj)
test weights: lw
adjRSlag = 1.3279, df = 1, p-value = 0.2492
Rao's score (a.k.a Lagrange multiplier) diagnostics for spatial
dependence
data:
model: lm(formula = formula_lag8, data = baza_przekroj)
test weights: lw
SARMA = 4.4805, df = 2, p-value = 0.1064
cat("Reguła decyzyjna: RSlag istotny & RSerr nie -> SAR; odwrotnie -> SEM;\n",
"oba istotne -> SDM/SARAR; oba nieistotne -> słaba struktura (patrz panel).\n")Reguła decyzyjna: RSlag istotny & RSerr nie -> SAR; odwrotnie -> SEM;
oba istotne -> SDM/SARAR; oba nieistotne -> słaba struktura (patrz panel).
OLS (R² = 0,49) jest łącznie istotny, ale istotna jest tylko temperatura (β = −1,99; p = 0,0013). Założenia KMNK są spełnione, jednak reszty wykazują istotną autokorelację przestrzenną (I = 0,175; p = 0,0046) — widać to na mapie jako sąsiadujące prowincje o tym samym znaku reszty. To formalna motywacja dla modeli przestrzennych. Testy ex-ante są słabe i niejednoznaczne, co odzwierciedla niską moc pojedynczego przekroju (n = 47) i przenosi rozstrzygnięcie na model panelowy.
Estymujemy rodzinę modeli (SAR, SEM, SDM, SARAR), porównujemy je kryterium AIC (tabela i wykres) i wybieramy najlepszy testami ex-post (LR). Dla modelu z członem autoregresyjnym liczymy efekty bezpośrednie/pośrednie.
model_sar <- lagsarlm(formula_lag8, data = baza_przekroj, listw = lw, zero.policy = TRUE)
model_sem <- errorsarlm(formula_lag8, data = baza_przekroj, listw = lw, zero.policy = TRUE)
model_sdm <- lagsarlm(formula_lag8, data = baza_przekroj, listw = lw, Durbin = TRUE, zero.policy = TRUE)
model_sarar <- sacsarlm(formula_lag8, data = baza_przekroj, listw = lw, zero.policy = TRUE)
tabela_cross <- data.frame(
Model = c("OLS (lag8)", "SAR", "SEM", "SDM", "SARAR"),
LogLik = c(as.numeric(logLik(model_ols_final)), as.numeric(logLik(model_sar)),
as.numeric(logLik(model_sem)), as.numeric(logLik(model_sdm)),
as.numeric(logLik(model_sarar))),
AIC = c(AIC(model_ols_final), AIC(model_sar), AIC(model_sem),
AIC(model_sdm), AIC(model_sarar))
)
cat("\nSELEKCJA PRZEKROJOWYCH MODELI PRZESTRZENNYCH (EX-POST)\n")
SELEKCJA PRZEKROJOWYCH MODELI PRZESTRZENNYCH (EX-POST)
Model LogLik AIC
1 OLS (lag8) -37.08863 88.17725
2 SAR -34.50470 85.00939
3 SEM -34.44371 84.88741
4 SDM -29.59781 85.19563
5 SARAR -34.31916 86.63833
# Wykres porównawczy AIC
par(mar = c(6, 5, 4, 2))
bp_c <- barplot(tabela_cross$AIC, names.arg = tabela_cross$Model, col = "steelblue",
cex.names = 0.85, ylim = c(0, max(tabela_cross$AIC) * 1.12),
main = "Porównanie modeli przekrojowych — kryterium AIC",
ylab = "AIC (niżej = lepiej)")
text(bp_c, tabela_cross$AIC, labels = round(tabela_cross$AIC, 1), pos = 3, cex = 0.75)AIC modeli przekrojowych: wszystkie biją OLS, lecz różnią się między sobą śladowo — pojedynczy przekrój nie rozstrzyga formy zależności.
par(mar = c(5, 4, 4, 2))
# Test LR: czy SDM redukuje się do SAR/SEM? (wybór formy funkcyjnej)
cat("\nTEST LR: SDM vs SAR\n")
TEST LR: SDM vs SAR
Likelihood ratio for spatial linear models
data:
Likelihood ratio = 9.8138, df = 5, p-value = 0.08069
sample estimates:
Log likelihood of model_sdm Log likelihood of model_sar
-29.59781 -34.50470
TEST LR: SDM vs SEM (test wspólnego czynnika)
Likelihood ratio for spatial linear models
data:
Likelihood ratio = 9.6918, df = 5, p-value = 0.08445
sample estimates:
Log likelihood of model_sdm Log likelihood of model_sem
-29.59781 -34.44371
PODSUMOWANIE NAJLEPSZEGO MODELU PRZEKROJOWEGO
najlepszy_cross <- tabela_cross$Model[which.min(tabela_cross$AIC)]
cat("Najniższe AIC:", najlepszy_cross, "\n")Najniższe AIC: SEM
# Efekty bezpośrednie/pośrednie dla przekrojowego SAR (jeśli to on wygrywa
# lub dla ilustracji mechanizmu spillover w ujęciu statycznym):
cat("\nEFEKTY (IMPACTS) DLA PRZEKROJOWEGO SAR\n")
EFEKTY (IMPACTS) DLA PRZEKROJOWEGO SAR
tryCatch({
set.seed(2024)
print(summary(impacts(model_sar, listw = lw, R = 999), zstats = TRUE, short = TRUE))
}, error = function(e) {
message("impacts() zawiodło (", e$message, ") — liczę efekty ręcznie.")
print(impacts_reczne(model_sar, lw))
})Impact measures (lag, exact):
Direct Indirect Total
log(Mean_Temp_lag8) dy/dx -1.44558216 -0.88741090 -2.3329931
log(Humidity_lag8) dy/dx 1.24120629 0.76194908 2.0031554
log(GDPpc) dy/dx 0.37081621 0.22763587 0.5984521
log(Density_numeric) dy/dx -0.05657157 -0.03472803 -0.0912996
log(Older_numeric) dy/dx -0.70847593 -0.43491770 -1.1433936
========================================================
Simulation results ( variance matrix):
========================================================
Simulated standard errors
Direct Indirect Total
log(Mean_Temp_lag8) dy/dx 0.54259637 0.62691815 0.9706110
log(Humidity_lag8) dy/dx 1.46397305 1.38849811 2.6949448
log(GDPpc) dy/dx 0.54831978 0.49413690 0.9875508
log(Density_numeric) dy/dx 0.09885545 0.09879502 0.1871390
log(Older_numeric) dy/dx 0.42224922 0.54000400 0.8778312
Simulated z-values:
Direct Indirect Total
log(Mean_Temp_lag8) dy/dx -2.6786936 -1.4901615 -2.4599545
log(Humidity_lag8) dy/dx 0.8408264 0.6408415 0.7869380
log(GDPpc) dy/dx 0.7285757 0.5221880 0.6658137
log(Density_numeric) dy/dx -0.5757161 -0.4693311 -0.5518905
log(Older_numeric) dy/dx -1.7709888 -1.0013096 -1.4678334
Simulated p-values:
Direct Indirect Total
log(Mean_Temp_lag8) dy/dx 0.007391 0.13618 0.013895
log(Humidity_lag8) dy/dx 0.400445 0.52163 0.431318
log(GDPpc) dy/dx 0.466261 0.60154 0.505530
log(Density_numeric) dy/dx 0.564807 0.63883 0.581023
log(Older_numeric) dy/dx 0.076563 0.31668 0.142149
model_cross_best <- switch(najlepszy_cross,
"SAR" = model_sar,
"SDM" = model_sdm,
"SARAR" = model_sarar,
NULL)
if (!is.null(model_cross_best) && najlepszy_cross != "SAR") {
cat("\nEFEKTY (IMPACTS) DLA NAJLEPSZEGO MODELU PRZEKROJOWEGO:",
najlepszy_cross, "---\n")
set.seed(2024)
print(summary(impacts(model_cross_best, listw = lw, R = 999),
zstats = TRUE, short = TRUE))
}Wszystkie modele przestrzenne biją OLS (o ~3 pkt AIC), lecz różnią się między sobą śladowo, a testy LR są nieistotne — przy n = 47 przekrój nie rozstrzyga formy zależności. Efekty SAR potwierdzają jednak istotny, ujemny wpływ temperatury (efekt całkowity −2,33; p = 0,014). Kluczowe rozstrzygnięcia zapadają w modelu panelowym.
Wykorzystujemy pełny panel (47 × 30). Test Hausmana rozstrzyga między efektami stałymi a losowymi, kryterium AIC porządkuje rodziny modeli (tabela + wykres), a tabela poniżej podsumowuje wybrany model SAR RE. Na końcu kontrola wspólnego trendu (two-way FE).
formula_panel <- log(Incidence) ~ log(Mean_Temp_lag8) + log(Humidity_lag8) +
log(GDPpc) + log(Density_numeric) + log(Older_numeric)
formula_panel_fe <- log(Incidence) ~ log(Mean_Temp_lag8) + log(Humidity_lag8)
# Pomocnik: bezpieczna estymacja spml (zwraca NULL i komunikat zamiast przerwać skrypt)
fit_spml <- function(formula, ...) {
tryCatch(
spml(formula, data = baza_panelowa, index = c("province", "Date"),
listw = lw, ...),
error = function(e) { message(" [pominięto model] ", e$message); NULL }
)
}
# 6.1 SAR panelowy: pooling / efekty stałe (FE, formuła zredukowana) / efekty losowe
sar_pool <- fit_spml(formula_panel, model = "pooling", lag = TRUE, spatial.error = "none")
sar_fe <- fit_spml(formula_panel_fe, model = "within", lag = TRUE, spatial.error = "none",
effect = "individual")
sar_re <- fit_spml(formula_panel, model = "random", lag = TRUE, spatial.error = "none")
# 6.2 SEM panelowy
sem_pool <- fit_spml(formula_panel, model = "pooling", lag = FALSE, spatial.error = "b")
sem_re <- fit_spml(formula_panel, model = "random", lag = FALSE, spatial.error = "b")
# 6.3 SARAR (SAC) panelowy
sarar_pool <- fit_spml(formula_panel, model = "pooling", lag = TRUE, spatial.error = "b")
sarar_re <- fit_spml(formula_panel, model = "random", lag = TRUE, spatial.error = "b")
# 6.4 Przestrzenny test Hausmana (FE vs RE) — wybór struktury efektów
# WAŻNE: test porównuje współczynniki WSPÓLNE dla obu modeli, więc RE do testu
# estymujemy na TEJ SAMEJ (zredukowanej) formule co FE — inaczej test jest błędny.
sar_re_h <- fit_spml(formula_panel_fe, model = "random", lag = TRUE, spatial.error = "none")
cat("\nPRZESTRZENNY TEST HAUSMANA (SAR: FE vs RE, zmienne zmienne w czasie)\n")
PRZESTRZENNY TEST HAUSMANA (SAR: FE vs RE, zmienne zmienne w czasie)
if (!is.null(sar_fe) && !is.null(sar_re_h)) {
print(sphtest(sar_fe, sar_re_h))
} else {
message("Test Hausmana pominięty (FE lub RE nie zostały oszacowane).")
}
Hausman test for spatial models
data: formula
chisq = 2.2101, df = 2, p-value = 0.3312
alternative hypothesis: one model is inconsistent
# 6.5 Funkcja licząca AIC dla obiektów splm (brak metody AIC w pakiecie)
aic_splm <- function(model, dodatkowe = 0) {
ll <- as.numeric(model$logLik)
k <- length(coef(model)) + dodatkowe # +1 SAR/SEM (rho/lambda), +2 SARAR
2 * k - 2 * ll
}
modele_panel <- list(
"SAR pooling" = sar_pool, "SAR within (FE)" = sar_fe,
"SAR random (RE)" = sar_re, "SEM pooling" = sem_pool,
"SEM random (RE)" = sem_re, "SARAR pooling" = sarar_pool,
"SARAR random (RE)" = sarar_re
)
dodatk_k <- c("SAR pooling" = 1, "SAR within (FE)" = 1, "SAR random (RE)" = 1,
"SEM pooling" = 1, "SEM random (RE)" = 1,
"SARAR pooling" = 2, "SARAR random (RE)" = 2)
# Usuwamy modele, które nie zostały oszacowane (NULL), zachowując nazwy
ok <- !vapply(modele_panel, is.null, logical(1))
modele_panel <- modele_panel[ok]
tabela_panel <- data.frame(
Model = names(modele_panel),
LogLik = vapply(modele_panel, function(m) as.numeric(m$logLik), numeric(1)),
AIC = mapply(function(m, k) aic_splm(m, k),
modele_panel, dodatk_k[names(modele_panel)]),
row.names = NULL
)
cat("\nSELEKCJA MODELI PANELOWYCH (KRYTERIA EX-POST)\n")
SELEKCJA MODELI PANELOWYCH (KRYTERIA EX-POST)
Model LogLik AIC
1 SAR pooling -1525.8498 3065.700
2 SAR within (FE) -656.5668 1321.134
3 SAR random (RE) -783.3613 1580.723
4 SEM pooling -1491.3270 2996.654
5 SEM random (RE) -778.3339 1570.668
6 SARAR pooling -1456.5847 2929.169
7 SARAR random (RE) -681.5822 1379.164
cat("UWAGA: 'SAR within (FE)' liczony na zredukowanej formule (tylko zmienne\n",
"zmienne w czasie) -> jego AIC NIE jest porównywalne z pooling/RE i jest\n",
"wyłączony z automatycznego wyboru najlepszego modelu.\n")UWAGA: 'SAR within (FE)' liczony na zredukowanej formule (tylko zmienne
zmienne w czasie) -> jego AIC NIE jest porównywalne z pooling/RE i jest
wyłączony z automatycznego wyboru najlepszego modelu.
par(mar = c(9, 5, 4, 2)) # szeroki dolny margines pod pionowe, długie nazwy modeli
barplot(tabela_panel$AIC, names.arg = tabela_panel$Model, las = 2,
col = "darkorange", cex.names = 0.8,
main = "Porównanie modeli panelowych — AIC", ylab = "AIC")AIC modeli panelowych: specyfikacja z efektami losowymi (RE) radykalnie przewyższa modele typu pooling.
par(mar = c(5, 4, 4, 2))
# --- WYBÓR FINALNEGO MODELU PANELOWEGO (ex-post + diagnostyka) ---
# Kandydat wg najniższego AIC (spośród modeli porównywalnych pooling/RE):
porownywalne <- !grepl("within", tabela_panel$Model)
idx_aic <- which(porownywalne)[which.min(tabela_panel$AIC[porownywalne])]
nazwa_aic <- tabela_panel$Model[idx_aic]
cat("\nKandydat wg najniższego AIC:", nazwa_aic, "\n")
Kandydat wg najniższego AIC: SARAR random (RE)
nazwa_best <- if ("SAR random (RE)" %in% names(modele_panel)) "SAR random (RE)" else nazwa_aic
idx_best <- match(nazwa_best, tabela_panel$Model)
cat("Finalny model interpretowany (po diagnostyce):", nazwa_best, "\n")Finalny model interpretowany (po diagnostyce): SAR random (RE)
model_najlepszy <- modele_panel[[nazwa_best]]
knitr::kable(tab_panel(model_najlepszy),
caption = "Model finalny SAR RE: lambda i elastycznosci (log-log)")| Estimate | Std. Error | t-value | Pr(>|t|) | |
|---|---|---|---|---|
| lambda (lag przestrz.) | 0.9302 | NA | NA | NA |
| (Intercept) | 0.2526 | 4.1570 | 0.0608 | 0.9515 |
| log(Mean_Temp_lag8) | -0.2558 | 0.0691 | -3.7005 | 0.0002 |
| log(Humidity_lag8) | 0.0785 | 0.0939 | 0.8357 | 0.4034 |
| log(GDPpc) | 0.3554 | 0.4051 | 0.8774 | 0.3803 |
| log(Density_numeric) | -0.0560 | 0.0714 | -0.7844 | 0.4328 |
| log(Older_numeric) | -1.0107 | 0.4351 | -2.3230 | 0.0202 |
sar_twoways <- fit_spml(formula_panel_fe, model = "within", lag = TRUE,
spatial.error = "none", effect = "twoways")
if (!is.null(sar_twoways)) {
cat("\nSAR PANELOWY: EFEKTY DWUKIERUNKOWE (jednostka + czas)\n")
print(summary(sar_twoways))
cat("\n(Wartość spatialnego lagu 'lambda' odczytujemy z wydruku summary powyżej.)\n")
}
SAR PANELOWY: EFEKTY DWUKIERUNKOWE (jednostka + czas)
Spatial panel fixed effects lag model
Call:
spml(formula = formula, data = baza_panelowa, index = c("province",
"Date"), listw = lw, model = "within", effect = "twoways",
lag = TRUE, spatial.error = "none")
Residuals:
Min. 1st Qu. Median 3rd Qu. Max.
-2.1670195 -0.1226429 0.0038046 0.1341532 1.5360505
Spatial autoregressive coefficient:
Estimate Std. Error t-value Pr(>|t|)
lambda 0.066160 0.038229 1.7306 0.08352 .
Coefficients:
Estimate Std. Error t-value Pr(>|t|)
log(Mean_Temp_lag8) 0.017081 0.097286 0.1756 0.8606
log(Humidity_lag8) -0.613436 0.101512 -6.0430 1.513e-09 ***
---
Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
(Wartość spatialnego lagu 'lambda' odczytujemy z wydruku summary powyżej.)
Hausman (p = 0,331) wskazuje efekty losowe (RE). Najniższe AIC ma SARAR RE, lecz jest numerycznie kruchy (błąd przestrzenny ρ ≈ 0,98, ujemny lag), więc jako finalny przyjmujemy SAR RE. W nim temperatura (−0,256; p = 0,0002) i odsetek 65+ (−1,011; p = 0,020) są istotne i ujemne; lag λ = 0,93 jest wysoki. Two-way FE pokazuje, że po usunięciu wspólnego trendu epidemii realny spillover między sąsiadami jest niewielki (λ ≈ 0,07) — duża część „autokorelacji” to wspólna trajektoria fali.
W modelu z członem autoregresyjnym surowych współczynników nie czyta się wprost — rozkładamy je na efekt bezpośredni (własna prowincja), pośredni (sąsiedzi) i całkowity (tabela + wykres).
liczba_okresow <- length(unique(baza_panelowa$Date))
efekty_df <- tryCatch(impacts_reczne(model_najlepszy, lw),
error = function(e) {
message("Nie udało się policzyć efektów: ", e$message); NULL })
cat("\nEFEKTY BEZPOŚREDNIE / POŚREDNIE / CAŁKOWITE (średnie, log-log)\n")
EFEKTY BEZPOŚREDNIE / POŚREDNIE / CAŁKOWITE (średnie, log-log)
Zmienna Bezposredni Posredni Calkowity
1 log(Mean_Temp_lag8) -0.4479 -3.2158 -3.6637
2 log(Humidity_lag8) 0.1375 0.9871 1.1246
3 log(GDPpc) 0.6224 4.4689 5.0914
4 log(Density_numeric) -0.0981 -0.7044 -0.8025
5 log(Older_numeric) -1.7700 -12.7084 -14.4783
# Wykres efektów bezpośrednich vs pośrednich
if (!is.null(efekty_df)) tryCatch({
m <- t(as.matrix(efekty_df[, c("Bezposredni", "Posredni")]))
colnames(m) <- efekty_df$Zmienna
rownames(m) <- c("Bezpośredni", "Pośredni")
par(mar = c(11, 5, 4, 2)) # dół: długie nazwy log(...)
yr <- range(c(0, m), na.rm = TRUE)
barplot(m, beside = TRUE, col = c("steelblue", "tomato"), las = 2,
cex.names = 0.75, ylim = c(yr[1] * 1.15, yr[2] * 1.15),
main = "Efekty bezpośrednie i pośrednie (elastyczności)",
ylab = "Elastyczność (model log-log)")
abline(h = 0, lty = 2)
legend("bottomleft", inset = c(0.02, 0.02), fill = c("steelblue", "tomato"),
legend = c("Bezpośredni", "Pośredni"), bty = "n", cex = 0.9)
par(mar = c(5, 4, 4, 2))
}, error = function(e) message("Pominięto wykres efektów: ", e$message))Efekty bezpośrednie i pośrednie: bezpośrednie są wiarygodne; pośrednie i całkowite zawyża wysoki lag (lambda=0,93) — nie czytamy ich dosłownie.
Efekty bezpośrednie są wiarygodne (temperatura −0,45; odsetek 65+ −1,77 — oba ujemne). Efekty całkowite są zawyżone przez wysoki lag (λ = 0,93 → mnożnik ~14×), więc nie interpretujemy ich dosłownie — to artefakt skumulowanej zmiennej Y, a nie 14-krotny spillover.
Powtarzamy estymację wybranego modelu na drugim, niezależnym zbiorze danych pogodowych (zachorowalność identyczna) i porównujemy współczynniki.
model_robust <- tryCatch(
spml(formula_panel, data = baza_panelowa_2,
index = c("province", "Date"), listw = lw,
model = gsub(".*(pooling|within|random).*", "\\1", tolower(nazwa_best)),
lag = grepl("SAR|SARAR", nazwa_best),
spatial.error = ifelse(grepl("SEM|SARAR", nazwa_best), "b", "none")),
error = function(e) { message("Model robust (zbiór 2) nie zbiegł: ", e$message); NULL }
)
cat("\nPOROWNANIE: ZBIÓR 1 vs ZBIÓR 2 (ten sam model)\n")
POROWNANIE: ZBIÓR 1 vs ZBIÓR 2 (ten sam model)
if (!is.null(model_robust)) {
porownanie_robust <- data.frame(
Parametr = names(coef(model_najlepszy)),
Zbior_1 = round(as.numeric(coef(model_najlepszy)), 4),
Zbior_2 = round(as.numeric(coef(model_robust))[seq_along(coef(model_najlepszy))], 4)
)
print(porownanie_robust)
} else {
message("Porównanie pominięte — model na zbiorze 2 nie został oszacowany.")
} Parametr Zbior_1 Zbior_2
1 (Intercept) 0.2526 0.7066
2 log(Mean_Temp_lag8) -0.2558 -0.2887
3 log(Humidity_lag8) 0.0785 0.0933
4 log(GDPpc) 0.3554 0.3290
5 log(Density_numeric) -0.0560 -0.0596
6 log(Older_numeric) -1.0107 -1.0667
Współczynniki są niemal identyczne w obu zbiorach (temperatura −0,256/−0,289; odsetek 65+ −1,011/−1,067). Odporność jest mocna — kierunkowe wnioski, zwłaszcza o ujemnym wpływie temperatury, są wiarygodne i nieczułe na źródło danych pogodowych.
Poniżej syntetyczne podsumowanie; pełne, liczbowe zestawienie generuje kod na końcu.
SYNTEZA LICZBOWA (wartości bieżące)
Liczba jednostek w analizie : 47
cat("Globalne I Morana (przekrój) :",
round(as.numeric(test_morana$estimate[1]), 4),
" p-value:", signif(test_morana$p.value, 3), "\n")Globalne I Morana (przekrój) : 0.2243 p-value: 0.00374
cat("I Morana — zakres w czasie :",
round(min(moran_w_czasie), 3), "..", round(max(moran_w_czasie), 3),
"( dni istotnych:", sum(pval_w_czasie < 0.05), "/", length(dni_all), ")\n")I Morana — zakres w czasie : 0.156 .. 0.243 ( dni istotnych: 30 / 30 )
cat("LISA istotne (p<0,05) bez/po FDR:",
sum(baza_przekroj$LISA_p < 0.05), "/",
sum(baza_przekroj$LISA_p_BH < 0.05), "\n")LISA istotne (p<0,05) bez/po FDR: 9 / 0
Najlepszy model przekrojowy AIC : SEM
Panel — najniższe AIC : SARAR random (RE)
Panel — model finalny (diagn.) : SAR random (RE)
cat("Reszty OLS — I Morana (p-value) :",
signif(lm.morantest(model_ols_final, lw, zero.policy = TRUE)$p.value, 3), "\n")Reszty OLS — I Morana (p-value) : 0.00461
Celem projektu było sprawdzenie, czy zachorowalność na COVID-19 w prowincjach Hiszpanii w pierwszej fali pandemii ma charakter przestrzenny, oraz wskazanie jej determinant. Przeprowadzona analiza pozwala odpowiedzieć twierdząco i — co istotne — rozróżnić wnioski pewne od tych wymagających ostrożności.
Najmocniejszym i jednoznacznym ustaleniem jest istnienie globalnej struktury przestrzennej zjawiska. Statystyka I Morana (0,224; p = 0,0037) jest dodatnia i istotna, potwierdzona testem permutacyjnym Monte Carlo, istotna w każdym z 30 dni okna badawczego i odporna na zmianę definicji sąsiedztwa (Queen vs kNN). Zachorowalność nie rozkłada się losowo, lecz koncentruje w zwartym rdzeniu centralnej Hiszpanii (obie Kastylie), z wyraźnym „chłodnym” obszarem na południu (zachodnia Andaluzja) — co zgodnie potwierdzają statystyki LISA i Getis-Ord Gi*. Uczciwie odnotowujemy przy tym, że po korekcie na wielokrotne testowanie (FDR) żaden pojedynczy klaster lokalny nie pozostaje indywidualnie istotny: pewny jest sygnał na poziomie całego kraju, lecz nie wskazania dla konkretnych prowincji.
W warstwie przyczynowej najbardziej wiarygodną determinantą okazała się temperatura: jej wpływ jest ujemny i istotny statystycznie zarówno w modelu przekrojowym (efekt całkowity SAR ≈ −2,33), jak i w panelowym (SAR RE: −0,256; p = 0,0002), a co najważniejsze — pozostaje stabilny po powtórzeniu estymacji na drugim, niezależnym zbiorze danych pogodowych. Wyższa temperatura wiąże się z niższą zachorowalnością, co jest spójne z sezonowością transmisji wirusów oddechowych. W modelu panelowym istotny okazał się również odsetek osób starszych (65+). Pozostałe czynniki (wilgotność, PKB per capita, gęstość zaludnienia) nie dały odpornych, jednoznacznych wyników.
Wybór modelu poprzedziliśmy diagnostyką: mimo najniższego AIC odrzuciliśmy model SARAR RE jako numerycznie niestabilny i jako finalny przyjęliśmy przestrzenny model panelowy z efektami losowymi (SAR RE), zgodny z testem Hausmana. Wysoka wartość parametru autoregresji (λ = 0,93) zawyża efekty całkowite, dlatego wnioski opieramy na efektach bezpośrednich. Kluczowa, samokrytyczna obserwacja płynie z modelu z efektami dwukierunkowymi (two-way FE): po usunięciu wspólnej, ogólnokrajowej trajektorii epidemii rzeczywiste „rozlewanie się” między sąsiadami okazuje się niewielkie (λ ≈ 0,07). Oznacza to, że znaczna część obserwowanej autokorelacji odzwierciedla wspólny trend fali, a nie bezpośrednią transmisję od prowincji do prowincji.
Główne ograniczenia analizy wynikają z natury danych: zmienna objaśniana ma charakter skumulowany i silnie trendujący, panel obejmuje krótkie 30-dniowe okno, a model nie uwzględnia mobilności ludności ani intensywności testowania. Naturalnym kierunkiem rozwoju jest oparcie modelu na dziennych przyrostach zachorowań zamiast wartości skumulowanych (co usunęłoby trend i prawdopodobnie sprowadziło parametr przestrzenny do realistycznego poziomu), włączenie danych o mobilności oraz zastosowanie dynamicznych modeli przestrzenno-czasowych.
Projekt wykazał, że rozkład COVID-19 w Hiszpanii ma wyraźną, odporną i trwałą w czasie strukturę przestrzenną, a temperatura jest jego stabilnym, ujemnym czynnikiem. Jednocześnie — dzięki testom odporności i kontroli wspólnego trendu — pokazaliśmy, gdzie przebiega granica między wnioskiem pewnym a problemem skumulowanych danych, co uznajemy za istotną wartość metodologiczną całej pracy.