Jak zważyć psa linijką w R

Jutro w ramach Bałtyckiego Festiwalu Nauki po raz kolejny poprowadzimy ze studentami Analityki Gospodarczej na Wydziale Zarządzania i Ekonomii PG zajęcia ze statystyki dla uczniów szkół podstawowych zatytułowane “Jak zważyć psa linijką”. Za materiały dziękujemy Brygadzie Miar.

Pierwszym zadaniem, z którym zmierzą się uczestnicy warsztatów, jest oszacowanie masy psa na podstawie jego wielkości. Poniżej fragment komiksu przygotowanego kilka lat temu przez prof. Przemysława Biecka.

Fragment z komiksu P. Biecka - szacowanie masy psa

Uczniowie będą to zadanie wykonywać “na oko”, jednak studenci po zajęciach ze statystyki mogą przeprowadzić obliczenia zgodnie z regułami sztuki. Oto wykres, który na podstawie prostej regresji dopasowanej metodą najmniejszych kwadratów przewiduje masę psa, którego spotkali Beta i Bit:

Poniżej przedstawiam propozycję rozwiązania zadania w R.

Stwórzmy najpierw tabelę (data frame, ramkę danych):

size<-c(20, 22, 40, 55, 55, 70, 71, 80)
mass<-c(2.7, 3, 13, 28, 31, 50, 70, 90)
labels<-c("Chihuahua", "Yorkshire", "Terier", "Bearded collie", 
          "Chow Chow", "Akita", "Nowofundland", "Mastif")
df<-data.frame(size, mass, labels)
df
##   size mass         labels
## 1   20  2.7      Chihuahua
## 2   22  3.0      Yorkshire
## 3   40 13.0         Terier
## 4   55 28.0 Bearded collie
## 5   55 31.0      Chow Chow
## 6   70 50.0          Akita
## 7   71 70.0   Nowofundland
## 8   80 90.0         Mastif

Przedstawmy dane na wykresie:

library(ggplot2)
library(ggrepel)
ggplot(data=df, aes(x=size, y=mass)) + 
  geom_point(size=4) +
  xlab("Wielkość [cm]") +
  ylab("Masa [kg]") +
  theme_bw() +
  geom_text_repel(data=df, aes(x=size, y=mass, label=labels))

Wygląda na to, że zależność pomiędzy wielkością a masą nie jest liniowa. Będziemy więc stosować model potęgowy. W takiej sytuacji, aby użyć regresji liniowej, zarówno zmienna X (objaśniająca, czyli wielkość) i zmienna Y (przewidywana, czyli masa) powinny zostać zlogarytmowane.

Nie powiemy tego dzieciom, ale w ich komiksie zarówno wielkość, jak i masa są przedstawione za pomocą skali logarytmicznej (proszę jeszcze raz spojrzeć na zamieszczony powyżej fragment komiksu). Spróbujmy to odtworzyć w R:

yticks<-c(2,3,4,5,6,7,8,9,10,15,20,25,(3:10)*10)
xticks<-c(20, 25, (3:10)*10)

ggplot(df, aes(x=size, y=mass)) + 
  geom_point(size=4) +
  xlab("Wielkość [cm]") +
  ylab("Masa [kg]") +
  scale_y_continuous(trans='log10', breaks=yticks) +
  scale_x_continuous(trans='log10', breaks=xticks) +
  theme_bw() +
  geom_text_repel(data=df, aes(x=size, y=mass, label=labels))

Jak widać, teraz poprowadzenie prostej linii w przybliżeniu dopasowanej do punktów jest możliwe.

Stwórzmy model potęgowy (model liniowy na zlogarytmowanych zmiennych):

model<-lm(log(mass)~log(size))
summary(model)
## 
## Call:
## lm(formula = log(mass) ~ log(size))
## 
## Residuals:
##      Min       1Q   Median       3Q      Max 
## -0.14068 -0.08472 -0.02180  0.10383  0.16000 
## 
## Coefficients:
##             Estimate Std. Error t value Pr(>|t|)    
## (Intercept)  -6.6678     0.3307  -20.16 9.66e-07 ***
## log(size)     2.5234     0.0855   29.52 1.00e-07 ***
## ---
## Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
## 
## Residual standard error: 0.1207 on 6 degrees of freedom
## Multiple R-squared:  0.9932,	Adjusted R-squared:  0.992 
## F-statistic: 871.1 on 1 and 6 DF,  p-value: 1.003e-07

Wygląda na to, że nasz model ma postać:

$$\widehat{\text{ln}Y} = -6{,}6678 + {2{,}5234} \text{ln}X,$$

czyli inaczej:

$$\widehat{Y} = e^{-6{,}6678} X ^{2{,}5234}$$

Sprawdźmy (używając komendy predict), co model przewiduje dla 60-kilogramowego psa:

(prediction<-exp(predict.lm(model, newdata=data.frame(size=c(60)))))
##        1 
## 39.00646

Model przewiduje, że pies będzie ważył około 39,01 kg. Matematycznie wyglądałoby to tak:

$$\widehat{Y} = e^{-6{,}6678} 60 ^{2{,}5234} \approx 39{,}01$$

Na wykresie można to przedstawić tak:

ggplot(df, aes(x=size, y=mass)) + 
  geom_point(size=4) +
  xlab("Wielkość [cm]") +
  ylab("Masa [kg]") +
  stat_smooth(method = "lm",
              formula = y ~ x,
              geom = "smooth", 
              se=FALSE, 
              colour='red') +
  scale_y_continuous(trans='log10', breaks=yticks) +
  scale_x_continuous(trans='log10', breaks=xticks) +
  theme_bw() +
  geom_point(data=data.frame(size=60, mass=prediction, labels="Nasz Pies"), 
             aes(x=size, y=mass), 
             colour='blue',
             size=4) + 
  geom_text_repel(data=df, aes(x=size, y=mass, label=labels))+
  geom_segment(aes(x=60, y=0, xend=60, yend=prediction), lty=2, col='blue')+
  geom_segment(aes(x=0, y=prediction, xend=60, yend=prediction), lty=2, col='blue') +
  annotate("text", x=min(xticks)-1, y=prediction+5, label=format(round(prediction,2), decimal.mark=',', nsmall=2), col='blue')

Jeżeli nie chcemy mieć skali logarytmicznej na osiach, wykres będzie wyglądał tak:

ggplot(df, aes(x=size, y=mass)) + 
  geom_point(size=4) +
  xlab("Wielkość [cm]") +
  ylab("Masa [kg]") +
  geom_function(fun=function(x){exp(model$coefficients[1])*x^model$coefficients[2]},
                colour='red') +
  scale_x_continuous(limits=c(0,90), breaks=sort(c(0:3*25, 60)), expand = c(0, 0)) +
  scale_y_continuous(limits=c(0,100), breaks=sort(c(0:4*25, as.numeric(round(prediction,2)))), labels = function(x){format(x,decimal.mark = ",", nsmall=2)}, expand = c(0, 0)) +
  theme_bw() +
  geom_point(data=data.frame(size=60, mass=prediction, labels="Nasz Pies"), 
             aes(x=size, y=mass), 
             colour='blue',
             size=4) + 
  geom_text_repel(data=df, aes(x=size, y=mass, label=labels))+
  geom_segment(aes(x=60, y=0, xend=60, yend=prediction), lty=2, col='blue')+
  geom_segment(aes(x=0, y=prediction, xend=60, yend=prediction), lty=2, col='blue')

Zamiast oszacowania punktowego możemy zaproponować oszacowanie w formie przedziału. Wymaga to oczywiście przyjęcia, że odpowiednie założenia (wykorzystana próba jest losowa, warunkowy rozkład logarytmu masy jest normalny itd.) przyjnajmniej w przybliżeniu są spełnione.

Przedział predykcji dla 60-centymetrowego psa możemy wyznaczyć w następujący sposób:

(prediction_interval<-exp(predict(
  model, 
  newdata=data.frame(size=c(60)), 
  interval = "prediction", 
  level=0.95))
 )
##        fit      lwr      upr
## 1 39.00646 28.38695 53.59872

Czyli na poziomie ufności 95% przewidujemy, że 60-centymetrowy pies będzie ważył pomiędzy 28,39 a 53,60 kg.

x4pred<-20:80
df3<-data.frame(exp(predict(model, newdata=data.frame(size=x4pred), interval = "prediction", level=0.95)))
df3$x<-x4pred

ggplot(df, aes(x=size, y=mass)) + 
  geom_point(size=4) +
  xlab("Wielkość [cm]") +
  ylab("Masa [kg]") +
  stat_smooth(method = "lm",
              formula = y ~ x,
              geom = "smooth", 
              se=FALSE, 
              colour='red') +
  scale_y_continuous(trans='log10', breaks=yticks) +
  scale_x_continuous(trans='log10', breaks=xticks) +
  theme_bw() +
  geom_point(data=data.frame(size=60, mass=prediction, labels="Nasz Pies"), 
             aes(x=size, y=mass), 
             colour='blue',
             size=4) + 
  geom_text_repel(data=df, aes(x=size, y=mass, label=labels)) +
  geom_segment(aes(x=60, y=0, xend=60, yend=prediction_interval[,3]), lty=2, col='blue') +
  geom_segment(aes(x=0, y=prediction, xend=60, yend=prediction), lty=2, col='blue') +
  annotate("text", x=min(xticks)-1, y=prediction+5, label=format(round(prediction,2), decimal.mark=',', nsmall=2), col='blue') + 
  geom_line(data=df3, aes(x=x, y=lwr), col='blue', lty=3) + 
  geom_line(data=df3, aes(x=x, y=upr), col='blue', lty=3) +
  geom_segment(aes(x=0, y=lower, xend=60, yend=lower), lty=2, col='blue') +
  annotate("text", x=min(xticks)-1, y=lower+3, label=format(lower, decimal.mark = ',', nsmall=2), col='blue') + 
  geom_segment(aes(x=0, y=upper, xend=60, yend=upper), lty=2, col='blue') +
  annotate("text", x=min(xticks)-1, y=upper+8, label=format(upper, decimal.mark=',', nsmall=2), col='blue')
ggplot(df, aes(x=size, y=mass)) + 
  geom_point(size=4) +
  xlab("Wielkość [cm]") +
  ylab("Masa [kg]") +
  geom_function(fun=function(x){exp(model$coefficients[1])*x^model$coefficients[2]},
                colour='red') +
  scale_x_continuous(limits=c(0,90), breaks=sort(c(0:3*25, 60)), expand = c(0, 0)) +
  scale_y_continuous(limits=c(0,100), breaks=sort(c(0:4*25, lower, upper, as.numeric(round(prediction,2)))), labels = function(x){format(x,decimal.mark = ",", nsmall=2)}, expand = c(0, 0)) +
  theme_bw() +
  geom_point(data=data.frame(size=60, mass=prediction, labels="Nasz Pies"), 
             aes(x=size, y=mass), 
             colour='blue',
             size=4) + 
  geom_text_repel(data=df, aes(x=size, y=mass, label=labels))+
  geom_segment(aes(x=60, y=0, xend=60, yend=upper), lty=2, col='blue')+
  geom_segment(aes(x=0, y=prediction, xend=60, yend=prediction), lty=2, col='blue')+ 
  geom_line(data=df3, aes(x=x, y=lwr), col='blue', lty=3) + 
  geom_line(data=df3, aes(x=x, y=upr), col='blue', lty=3) +
  geom_segment(aes(x=0, y=lower, xend=60, yend=lower), lty=2, col='blue') +
  geom_segment(aes(x=0, y=upper, xend=60, yend=upper), lty=2, col='blue') 

Uzupełnienie 25.05.2024:

W 2024 ważyliśmy Tinę, suczkę pani Dagmary. Tina ma 30 cm.

(prediction_interval<-exp(predict(
  model, 
  newdata=data.frame(size=c(30)), 
  interval = "prediction", 
  level=0.95))
 )
##        fit      lwr      upr
## 1 6.784646 4.896905 9.400107

Na poziomie ufności 95% przewidujemy, że 30-centymetrowy pies będzie ważył pomiędzy 4,90 a 9,40 kg.

x4pred<-20:80
prediction<-exp(predict.lm(model, newdata=data.frame(size=c(30))))

ggplot(df, aes(x=size, y=mass)) + 
  geom_point(size=4) +
  xlab("Wielkość [cm]") +
  ylab("Masa [kg]") +
  stat_smooth(method = "lm",
              formula = y ~ x,
              geom = "smooth", 
              se=FALSE, 
              colour='red') +
  scale_y_continuous(trans='log10', breaks=yticks) +
  scale_x_continuous(trans='log10', breaks=xticks) +
  theme_bw() +
  geom_point(data=data.frame(size=30, mass=prediction, labels="Tina"), 
             aes(x=size, y=mass), 
             colour='blue',
             size=4) + 
  geom_text_repel(data=df, aes(x=size, y=mass, label=labels)) +
  geom_segment(aes(x=30, y=0, xend=30, yend=prediction_interval[,3]), lty=2, col='blue') +
  geom_segment(aes(x=0, y=prediction, xend=30, yend=prediction), lty=2, col='blue') +
  annotate("text", x=min(xticks)-1, y=prediction+.5, label=format(round(prediction,2), decimal.mark=',', nsmall=2), col='blue') + 
  geom_line(data=df3, aes(x=x, y=lwr), col='blue', lty=3) + 
  geom_line(data=df3, aes(x=x, y=upr), col='blue', lty=3) +
  geom_segment(aes(x=0, y=lower, xend=30, yend=lower), lty=2, col='blue') +
  annotate("text", x=min(xticks)-1, y=lower+.3, label=format(lower, decimal.mark = ',', nsmall=2), col='blue') + 
  geom_segment(aes(x=0, y=upper, xend=30, yend=upper), lty=2, col='blue') +
  annotate("text", x=min(xticks)-1, y=upper+.8, label=format(upper, decimal.mark=',', nsmall=2), col='blue')
Błażej Kochański
Błażej Kochański
ekspert ds. ryzyka bankowego, naukowiec, menedżer i konsultant