W niniejszym projekcie zajmiemy się problematyką zastosowania logarytmów w statystyce. W tym celu skorzystamy z danych airquality, które są wbudowane w base R (bez dodatkowych pakietów). Zbiór airquality zawiera m.in. zmienną Ozone – stężenie ozonu (ppb) w Nowym Jorku. Od razu wyliczymy nową zmienną lnOzone która będzie zlogarytmowaną zmienną Ozone. Na początek porównajmy obie te zmienne.
data(airquality)
df = airquality %>%
as_tibble() %>%
filter(!is.na(Ozone)) %>%
mutate(lnOzone = log(Ozone))%>%
pivot_longer(c("Ozone", "lnOzone"), names_to = "data", values_to = "value")
df %>%
ggplot(aes(0,value ))+
geom_boxplot()+
geom_violin(alpha=0.2, fill="red")+
facet_wrap(vars(data), scales="free")
df %>% group_by(data) %>%
summarise(
min = min(value),
q1 = quantile(value, .25),
median = median(value),
mean = mean(value),
q3 = quantile(value, .75),
max = max(value),
var = var(value),
kurtoza = kurtosis(value),
shapiro.p = shapiro.test(value)$p
)
## # A tibble: 2 × 10
## data min q1 median mean q3 max var kurtoza shapiro.p
## <chr> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl>
## 1 Ozone 1 18 31.5 42.1 63.2 168 1088. 1.11 0.0000000279
## 2 lnOzone 0 2.89 3.45 3.42 4.15 5.12 0.749 0.776 0.0147
Skupmy się najpierw na wariancji. To jest kluczowy punkt.
Ozone:
lnOzone:
Wniosek: Logarytm zmienił strukturę błędu z multiplikatywnej na addytywną.
Przyjmijmy roboczo:
Tu:
Ozone: 1.11 → wyraźnie „spiczasty”, ciężkie ogonylnOzone: 0.776 → znacznie bliżej
normalnościTo nie jest cud, tylko: logarytm ścina prawy ogon, który był odpowiedzialny za nadmiar masy w ogonach.
Formalnie poziom p-value nie pozwala nam przyjąć hipotezy zerowej o normalności rozkładu w żadnym z przypadków. Możemy jedynie zauważyć, że p-value wzrosło z ~10⁻⁸ do ~10⁻² co jest ogromną poprawą jakości danych, nawet jeśli formalnie H₀ nadal jest odrzucana.
Transformacja logarytmiczna zmiennej Ozone istotnie zmniejszyła asymetrię rozkładu, ustabilizowała wariancję oraz przybliżyła rozkład do normalnego.
Q–Q plot porównuje empiryczne kwantyle danych z teoretycznymi kwantylami rozkładu normalnego. Im bliżej prostej, tym bliżej normalności.
Odchylenia:
S-kształt → skośność
odjazdy w ogonach → ciężkie ogony / obserwacje ekstremalne
krzywizna → zła transformacja skali
df %>%
ggplot(aes(sample = value)) +
stat_qq() +
stat_qq_line() +
facet_wrap(vars(data), scales = "free") +
labs(title = "Q–Q plot: Ozone vs ln(Ozone)")
Ozone (bez transformacji)Tu widać kilka rzeczy bardzo wyraźnie:
📌 Wniosek
Rozkład Ozone nie tylko nie jest normalny,
ale też:
lnOzone (po logarytmizacji)Tu obraz jest radykalnie inny:
📌 Wniosek
lnOzone jest:
Podsumowując
dla Ozone:
dla lnOzone:
Wykresy Q–Q jednoznacznie wskazują, że transformacja logarytmiczna zmiennej Ozone istotnie poprawia zgodność rozkładu z normalnym, eliminując silną prawoskośność oraz ciężki prawy ogon obserwowany w danych surowych.
df2 <- airquality %>%
as_tibble() %>%
filter(!is.na(Ozone), Month %in% c(5, 8)) %>%
mutate(
lnOzone = log(Ozone),
Month = factor(Month, labels = c("May", "Aug"))
)
ggplot(df2, aes(Month, lnOzone)) +
geom_violin(fill = "gray80") +
geom_boxplot(width = 0.2) +
labs(title = "ln(Ozone): May vs August")
Ten wykres mówi bardzo dużo i mówi to czytelnie:
Maj (May):
niższy poziom centralny (median,
mean)
większa koncentracja obserwacji w okolicach ~2.5–3.2
Sierpień (Aug):
- wyraźnie przesunięty rozkład w górę
- środek rozkładu ok. \~3.8–4.1
Najważniejsze:
przesunięcie jest globalne, a nie wynika z kilku ekstremów
kształty violinów są podobne → różnica dotyczy poziomu, nie tylko wariancji
To już samo w sobie uzasadnia test porównawczy.
lnOzonet.test(lnOzone ~ Month, data = df2)
##
## Welch Two Sample t-test
##
## data: lnOzone by Month
## t = -4.3542, df = 48.503, p-value = 6.881e-05
## alternative hypothesis: true difference in means between group May and group Aug is not equal to 0
## 95 percent confidence interval:
## -1.5102754 -0.5562632
## sample estimates:
## mean in group May mean in group Aug
## 2.812076 3.845345
różnica średnich jest wyraźna i stabilna
przedział ufności nie obejmuje zera
znak ujemny → May < August
To jest klasyczny przypadek, w którym:
dane po transformacji spełniają założenia wystarczająco dobrze,
Welch t-test jest najlepszym wyborem parametrycznym.
Ponieważ test był na logarytmach, różnica średnich:
\(\Delta = 3.845 − 2.812 \approx 1.03\)
oznacza:
\(\exp(1.03) \approx 2.8\)
📌 Interpretacja praktyczna
Średni poziom ozonu w sierpniu jest około 2.8 raza wyższy niż w maju.
To jest bardzo silny efekt, nie tylko „istotny statystycznie”.
Ozone (bez logarytmu)wilcox.test(Ozone ~ Month, data = df2)
## Warning in wilcox.test.default(x = DATA[[1L]], y = DATA[[2L]], ...): nie można
## obliczyć dokładnej wartości prawdopodobieństwa z powtórzonymi wartościami
##
## Wilcoxon rank sum test with continuity correction
##
## data: Ozone by Month
## W = 127.5, p-value = 0.0001208
## alternative hypothesis: true location shift is not equal to 0
p-value = 0.0001208
I to jest dokładnie to, czego należało się spodziewać.
Bo:
Wilcoxon mówi:
„Niezależnie od kształtu rozkładu, obserwacje z sierpnia są systematycznie większe niż z maja.”
Dane surowe są silnie skośne, ale różnica między miesiącami jest na tyle wyraźna, że wykrywa ją nawet test nieparametryczny.
Po transformacji logarytmicznej możliwe jest zastosowanie testu parametrycznego, który dodatkowo umożliwia interpretację różnicy w kategoriach względnych (ilorazów).
Rozdzielmy teraz dane na dwa podzbiory:
Dane dla których temperatura była większa lub równa medianie temperatury (High temp)
Dane dla których temperatura była niższa niż mediana temperatury (Low temp)
df3 <- airquality %>%
as_tibble() %>%
filter(!is.na(Ozone)) %>%
mutate(
lnOzone = log(Ozone),
TempGroup = if_else(Temp >= median(Temp, na.rm = TRUE),
"High temp", "Low temp")
)
ggplot(df3, aes(TempGroup, lnOzone)) +
geom_violin(fill = "gray80") +
geom_boxplot(width = 0.2) +
labs(title = "ln(Ozone): High vs Low")
Z wykresu ln(Ozone): High vs Low widać
jednoznacznie:
High temp:
median,
mean)Low temp:
Kluczowa obserwacja:
Rozkłady są do siebie podobne kształtem, ale przesunięte względem siebie.
To dokładnie ta sytuacja, w której:
lnOzonet.test(lnOzone ~ TempGroup, data = df3)
##
## Welch Two Sample t-test
##
## data: lnOzone by TempGroup
## t = 9.5139, df = 113.17, p-value = 4.015e-16
## alternative hypothesis: true difference in means between group High temp and group Low temp is not equal to 0
## 95 percent confidence interval:
## 0.9086325 1.3865825
## sample estimates:
## mean in group High temp mean in group Low temp
## 3.972533 2.824925
To jest wynik bardzo mocny statystycznie:
Różnica średnich logarytmów:
\(\Delta = 3.973 - 2.825 \approx 1.15\)
Po powrocie na skalę oryginalną:
\(\exp(1.15) \approx 3.16\)
📌 Interpretacja praktyczna
Przy wysokiej temperaturze średni poziom ozonu jest około trzykrotnie wyższy niż przy niskiej temperaturze.
To nie jest subtelny efekt — to silna zależność fizyczno-chemiczna, zgodna z mechanizmem fotochemicznego powstawania ozonu.
Ozonewilcox.test(Ozone ~ TempGroup, data = df3)
##
## Wilcoxon rank sum test with continuity correction
##
## data: Ozone by TempGroup
## W = 3041, p-value = 5.536e-14
## alternative hypothesis: true location shift is not equal to 0
Znów:
To dokładnie to, czego oczekujemy przy:
Zarówno analiza wizualna, jak i testy statystyczne jednoznacznie wskazują, że wysoka temperatura wiąże się z istotnie wyższym poziomem ozonu. Po transformacji logarytmicznej możliwe jest ilościowe ujęcie tego efektu — średnie stężenie ozonu przy wysokiej temperaturze jest około trzykrotnie wyższe niż przy temperaturze niskiej.
Zbudujmy dwa modele regresji liniowej.
Ozone ~ Temp + Winddfw = df %>% pivot_wider(names_from = data, values_from = value)
lmlnOzone <- lm(lnOzone ~ Temp + Wind, data = dfw)
lmOzone <- lm(Ozone ~ Temp + Wind, data = dfw)
summary(lmOzone)
##
## Call:
## lm(formula = Ozone ~ Temp + Wind, data = dfw)
##
## Residuals:
## Min 1Q Median 3Q Max
## -41.251 -13.695 -2.856 11.390 100.367
##
## Coefficients:
## Estimate Std. Error t value Pr(>|t|)
## (Intercept) -71.0332 23.5780 -3.013 0.0032 **
## Temp 1.8402 0.2500 7.362 3.15e-11 ***
## Wind -3.0555 0.6633 -4.607 1.08e-05 ***
## ---
## Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
##
## Residual standard error: 21.85 on 113 degrees of freedom
## Multiple R-squared: 0.5687, Adjusted R-squared: 0.5611
## F-statistic: 74.5 on 2 and 113 DF, p-value: < 2.2e-16
ln(Ozone) ~ Temp + Windsummary(lmlnOzone)
##
## Call:
## lm(formula = lnOzone ~ Temp + Wind, data = dfw)
##
## Residuals:
## Min 1Q Median 3Q Max
## -2.34415 -0.25774 0.03003 0.35048 1.18640
##
## Coefficients:
## Estimate Std. Error t value Pr(>|t|)
## (Intercept) -0.531932 0.608901 -0.874 0.38419
## Temp 0.057384 0.006455 8.889 1.13e-14 ***
## Wind -0.052534 0.017128 -3.067 0.00271 **
## ---
## Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
##
## Residual standard error: 0.5644 on 113 degrees of freedom
## Multiple R-squared: 0.5821, Adjusted R-squared: 0.5747
## F-statistic: 78.71 on 2 and 113 DF, p-value: < 2.2e-16
lmOzone)Temp = 1.84
Wind = -3.06
+1°C temperatury
→ średnio +1.84 ppb ozonu, niezależnie od
aktualnego poziomu
+1 jednostka wiatru
→ średnio −3.06 ppb ozonu, niezależnie od
aktualnego poziomu
Bo model mówi absurdalne rzeczy typu:
📌 Ten model zakłada stały, addytywny wpływ, co:
lmlnOzone) — TU
JEST KLUCZTemp = 0.0574
Wind = -0.0525
To są współczynniki logarytmiczne, czyli efekty względne.
\(\beta_{Temp}=0.0574\)
To znaczy:
\(\ln(Ozone + 1°C) - \ln(Ozone) = 0.0574 \cdot ln(Ozone+1°C)−ln(Ozone)=0.0574\)
Po przejściu na skalę oryginalną:
\(\exp(0.0574) \approx 1.059\)
📌 Interpretacja praktyczna
Każdy wzrost temperatury o 1°C zwiększa średni poziom ozonu o około 5.9%.
\(\beta_{Wind}=-0.0525\) → \(\exp(-0.0525) \approx 0.949\)
📌 Interpretacja praktyczna
Każdy wzrost prędkości wiatru o 1 jednostkę zmniejsza średni poziom ozonu o około 5%.
To jest:
Model
ln(Ozone) ~ Temp + Windzakłada, że czynniki meteorologiczne wpływają na ozon w sposób względny (procentowy), a nie addytywny, co prowadzi do stabilniejszej wariancji reszt i interpretowalnych parametrów.
Z modelu logarytmicznego:
\(\widehat{Ozone} = \exp(-0.53) \cdot \exp(0.057 \cdot Temp) \cdot \exp(-0.053 \cdot Wind)\)
Czyli:
ln(Ozone) jest
poprawnyplotlnOzone = dfw %>% ggplot(aes(Temp, Wind, colour = lnOzone))+
geom_point(size = 5)+
labs(title = "lm lnOzone ~ Temp + Win")
plotOzone = dfw %>% ggplot(aes(Temp, Wind, colour = Ozone))+
geom_point(size = 5)+
labs(title = "lm Ozone ~ Temp + Win")
grid.arrange(plotlnOzone, plotOzone, ncol = 2)
W obu wykresach widać ten sam fizyczny mechanizm:
Czyli:
To potwierdza:
lnOzone (model log-liniowy)Tu jest kluczowa różnica jakościowa.
Co widać:
Płynny, niemal liniowy gradient koloru
Jednorodna dynamika zmian
Brak dominacji pojedynczych obserwacji
📌 Interpretacja
Zależność ln(Ozone) od Temp i Wind jest bliska liniowej w całym zakresie danych.
To dokładnie tłumaczy:
Ozone (model surowy)Tutaj dokładnie widać, dlaczego ten model jest problematyczny.
Co widać:
Kilka punktów o bardzo wysokim Ozone dominuje skalę
Brak jednorodnego gradientu
Silna heteroscedastyczność wizualna
📌 Interpretacja
Model addytywny jest „ciągnięty” przez kilka ekstremalnych obserwacji i nie opisuje równomiernie całej przestrzeni danych.
To jest wizualny dowód, że:
logarytmizacja nie była zabiegiem kosmetycznym, tylko poprawą geometrii problemu.
W przestrzeni Temp–Wind zależność logarytmu stężenia ozonu wykazuje jednolity, liniowy charakter, podczas gdy model na danych surowych jest zdominowany przez obserwacje ekstremalne i charakteryzuje się niestabilną wariancją.
diaglnOzone = diagPlot(lmlnOzone)
diagOzone = diagPlot(lmOzone)
grid.arrange(diaglnOzone$rvn, diagOzone$rvn, ncol = 2)
Ten wykres odpowiada na trzy pytania naraz:
lnOzone ~ Temp + WindCo widać bardzo wyraźnie:
📌 Interpretacja statystyczna
To dokładnie odpowiada:
Ozone ~ Temp + WindTutaj różnica jest uderzająca.
Co widać:
reszty rozciągają się nawet do ±100
większość punktów mieści się w przedziale ±25, ale:
kilka obserwacji (np. 82) dominuje cały wykres
rozrzut reszt rośnie wraz z poziomem dopasowania (co już widzieliśmy wcześniej)
📌 Interpretacja statystyczna
To idealnie tłumaczy:
W modelu log-liniowym błędy są względne, stabilne i jednorodne, natomiast w modelu na danych surowych błędy rosną wraz z poziomem zmiennej i są zdominowane przez obserwacje ekstremalne.
Testy, R², p-value można czasem dyskutować.
Reszt nie da się oszukać.
Jeżeli:
to model jest praktycznie użyteczny.
I dokładnie to widać dla lnOzone ~ Temp + Wind.
Analiza reszt jednoznacznie wskazuje, że model log-liniowy zapewnia stabilną skalę błędu i jednorodne dopasowanie w całym zakresie danych, podczas gdy model na danych surowych charakteryzuje się dużymi, niestabilnymi resztami i wyraźną heteroscedastycznością.
grid.arrange(diaglnOzone$rvf, diagOzone$rvf, ncol = 2)
## `geom_smooth()` using formula = 'y ~ x'
## `geom_smooth()` using formula = 'y ~ x'
Sprawdza:
Czyli odpowiada na pytanie:
Czy model systematycznie się myli w pewnych zakresach wartości?
lnOzone ~ Temp + WindCo nowego tu widać:
Reszty są rozłożone wokół zera w całym zakresie fitted
Loess:
To oznacza:
📌 To jest odpowiedź na inne pytanie niż poprzedni wykres:
Czy model liniowy w ogóle ma sens w tej skali?
Odpowiedź: tak.
Ozone ~ Temp + WindTutaj wykres jest bardzo informatywny, nawet jeśli „na pierwszy rzut oka” przypomina poprzedni.
Co widać bardzo wyraźnie:
Wyraźny wzór U / S
Loess pokazuje systematyczne odchylenia:
Rosnąca wariancja reszt
Ekstremy skupione po jednej stronie
📌 To mówi coś innego niż „reszty vs index”:
Model jest strukturalnie źle postawiony – nawet po uwzględnieniu Temp i Wind.
To jest subtelne, ale bardzo ważne:
w lnOzone:
w Ozone:
krzywizna jest systematyczna
to sygnał:
Wykres reszt w funkcji wartości dopasowanych wskazuje, że model log-liniowy nie wykazuje istotnej nieliniowości ani heteroscedastyczności, podczas gdy model na danych surowych charakteryzuje się wyraźną strukturą reszt i niestabilną wariancją.